pipeline { agent { label 'kt-node-agent' } options { buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '30')) disableConcurrentBuilds() skipDefaultCheckout(true) timestamps() timeout(time: 90, unit: 'MINUTES') } parameters { string(name: 'NAPCAT_FORK_REF', defaultValue: 'main', description: 'Reviewed NapCatQQ fork branch, tag, or commit.') string(name: 'UPSTREAM_RELEASE_TAG', defaultValue: 'unknown', description: 'Reviewed upstream release tag for marker metadata.') string(name: 'UPSTREAM_RELEASE_COMMIT', defaultValue: '5c18a62530d87dbadf53d267002894faa6ca7e90', description: 'Reviewed upstream release commit for marker metadata.') string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Explicit runtime image tag to build, for example kt-napcat-desktop-cn:desktop-cn-v9.') string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Explicit desktop profile version paired with the runtime image tag.') string(name: 'NAPCAT_BASE_IMAGE_DIGEST', defaultValue: 'mlikiowa/napcat-docker@sha256:9254ec12af101576c5eeb4910847abd1d219297bc6d9a35c52511e12500f0f45', description: 'Immutable NapCat base image digest used for Docker build.') string(name: 'KT_ARTIFACT_ROOT', defaultValue: '/vol1/docker/kt-codex/artifacts/napcat-runtime-release', description: 'NAS artifact root for runtime release evidence.') } environment { KT_WORKSPACE_ROOT = '/vol1/docker/kt-codex/workspace/KT' API_ROOT = '/vol1/docker/kt-codex/workspace/KT/Node/kt-template-online-api' API_GIT_REFERENCE_HOST = '/vol1/docker/volumes/kt-node-agent-workdir/_data/workspace/KT-Template_KT-Template-API_main/.git' API_GIT_REFERENCE = '/ci/kt-template-online-api-main.git' NAPCAT_GIT_REPOSITORY = '/vol1/docker/kt-codex/git/NapCatQQ.git' KT_WORKFLOW_RUNNER_IMAGE = 'kt-jenkins-node-agent:current' } stages { stage('Validate parameters') { steps { script { requireRuntimeImageParameters( params.NAPCAT_FORK_REF, params.UPSTREAM_RELEASE_COMMIT, params.QQBOT_NAPCAT_IMAGE_OVERRIDE, params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE, params.NAPCAT_BASE_IMAGE_DIGEST ) requireImageProfileMatch( params.QQBOT_NAPCAT_IMAGE_OVERRIDE, params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE ) } } } stage('Build and verify runtime image') { steps { sh ''' set -e docker run --rm \ -e API_ROOT="$API_ROOT" \ -e API_GIT_REFERENCE="$API_GIT_REFERENCE" \ -e BUILD_TAG="$BUILD_TAG" \ -e BUILD_URL="$BUILD_URL" \ -e COREPACK_HOME=/vol1/docker/kt-codex/runtime/corepack \ -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ -e KT_WORKSPACE_ROOT="$KT_WORKSPACE_ROOT" \ -e NAPCAT_BASE_IMAGE_DIGEST="$NAPCAT_BASE_IMAGE_DIGEST" \ -e NAPCAT_FORK_REF="$NAPCAT_FORK_REF" \ -e NAPCAT_GIT_REPOSITORY="$NAPCAT_GIT_REPOSITORY" \ -e QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE="$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE" \ -e QQBOT_NAPCAT_IMAGE_OVERRIDE="$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ -e UPSTREAM_RELEASE_COMMIT="$UPSTREAM_RELEASE_COMMIT" \ -e UPSTREAM_RELEASE_TAG="$UPSTREAM_RELEASE_TAG" \ -v /vol1/docker/kt-codex:/vol1/docker/kt-codex \ -v "$API_GIT_REFERENCE_HOST":"$API_GIT_REFERENCE":ro \ -v /var/run/docker.sock:/var/run/docker.sock \ --user 0:0 \ --entrypoint bash "$KT_WORKFLOW_RUNNER_IMAGE" -lc ' set -euo pipefail source /vol1/docker/kt-codex/runtime/nvm/nvm.sh corepack prepare pnpm@10.28.2 --activate ARTIFACTS_BASE_REAL=$(realpath -m /vol1/docker/kt-codex/artifacts) ARTIFACT_ROOT_REAL=$(realpath -m "$KT_ARTIFACT_ROOT") case "$ARTIFACT_ROOT_REAL" in "$ARTIFACTS_BASE_REAL"/*) ;; *) echo "KT_ARTIFACT_ROOT must stay under /vol1/docker/kt-codex/artifacts" >&2 exit 2 ;; esac BUILD_ID_SAFE=$(printf "%s" "${BUILD_TAG:-manual}" | tr "/ " "__") ARTIFACT_DIR="$ARTIFACT_ROOT_REAL/runtime-image-build" BUILD_WORKDIR="$ARTIFACT_ROOT_REAL/runtime-image-build-workspace/$BUILD_ID_SAFE" NAPCAT_ROOT="$BUILD_WORKDIR/NapCatQQ" STAGE_OUT=".kt-workspace/napcat-desktop-cn-build-runtime-image" VERIFY_NAME="kt-napcat-runtime-image-verify-$BUILD_ID_SAFE" cleanup() { docker rm -f "$VERIFY_NAME" >/dev/null 2>&1 || true rm -rf "$BUILD_WORKDIR" } trap cleanup EXIT git config --global --add safe.directory "$NAPCAT_GIT_REPOSITORY" git config --global --add safe.directory "$API_ROOT" rm -rf "$ARTIFACT_DIR" "$BUILD_WORKDIR" mkdir -p "$ARTIFACT_DIR" "$BUILD_WORKDIR" if [ -n "$(git -C "$API_ROOT" status --short)" ]; then echo "API_ROOT must be clean before runtime image build: $API_ROOT" >&2 git -C "$API_ROOT" status --short >&2 exit 2 fi test -d "$API_GIT_REFERENCE" git -C "$API_ROOT" fetch "$API_GIT_REFERENCE" refs/remotes/origin/main git -C "$API_ROOT" checkout main git -C "$API_ROOT" merge --ff-only FETCH_HEAD git -C "$API_ROOT" rev-parse HEAD > "$ARTIFACT_DIR/api-root-commit.txt" git clone "$NAPCAT_GIT_REPOSITORY" "$NAPCAT_ROOT" cd "$NAPCAT_ROOT" git checkout --detach "$NAPCAT_FORK_REF" git rev-parse HEAD > "$ARTIFACT_DIR/fork-commit.txt" git status --short > "$ARTIFACT_DIR/fork-status.txt" pnpm install --frozen-lockfile pnpm --filter napcat-webui-frontend run build pnpm run build:shell pnpm run build:framework cd "$API_ROOT" node scripts/napcat-desktop-cn-stage-build.mjs \ --napcat-root "$NAPCAT_ROOT" \ --out "$STAGE_OUT" \ --upstream-release-tag "$UPSTREAM_RELEASE_TAG" \ --upstream-release-commit "$UPSTREAM_RELEASE_COMMIT" \ --napcat-base-image-digest "$NAPCAT_BASE_IMAGE_DIGEST" \ --jenkins-build-url "$BUILD_URL" \ | tee "$ARTIFACT_DIR/stage-build.json" cp "$STAGE_OUT/ci/napcat-desktop-cn/fork-artifact.json" "$ARTIFACT_DIR/fork-artifact.json" docker build \ --build-arg NAPCAT_BASE_IMAGE="$NAPCAT_BASE_IMAGE_DIGEST" \ -t "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ -f "$STAGE_OUT/ci/napcat-desktop-cn/Dockerfile" \ "$STAGE_OUT" docker run -d --name "$VERIFY_NAME" \ --cap-add SYS_ADMIN \ --security-opt apparmor=unconfined \ --security-opt seccomp=unconfined \ -e NAPCAT_REQUIRE_DEVICE_PROFILE=1 \ "$QQBOT_NAPCAT_IMAGE_OVERRIDE" >/dev/null sleep 3 docker exec "$VERIFY_NAME" sh /ci/napcat-desktop-cn/verify.sh docker image inspect "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ --format "tag={{index .RepoTags 0}} id={{.Id}} size={{.Size}} created={{.Created}} repoDigests={{json .RepoDigests}}" \ | tee "$ARTIFACT_DIR/image.txt" ' ''' } } } post { always { sh ''' set +e docker run --rm \ -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ -e WORKSPACE="$WORKSPACE" \ -v /vol1/docker/kt-codex:/vol1/docker/kt-codex \ -v kt-node-agent-workdir:/home/jenkins/agent \ --entrypoint bash "$KT_WORKFLOW_RUNNER_IMAGE" -lc ' set +e rm -rf "$WORKSPACE/artifacts" mkdir -p "$WORKSPACE/artifacts/napcat-runtime-image-build" cp -a "$KT_ARTIFACT_ROOT/runtime-image-build/." "$WORKSPACE/artifacts/napcat-runtime-image-build/" 2>/dev/null || true ' ''' archiveArtifacts artifacts: 'artifacts/napcat-runtime-image-build/**', allowEmptyArchive: true, fingerprint: false } } } /** * Stops runtime image builds unless all immutable release parameters are present. * @param forkRef Reviewed fork branch, tag, or commit that is cloned into the disposable build workspace. * @param upstreamReleaseCommit Reviewed upstream release commit stored in the image marker. * @param imageTag Reviewed image tag that will be created on NAS Docker. * @param profile Reviewed desktop profile version paired with the image tag. * @param baseImageDigest Immutable upstream/base Docker image digest in `repo@sha256` form. * @return Nothing; aborts this pipeline through `error` when required values are missing. */ void requireRuntimeImageParameters(String forkRef, String upstreamReleaseCommit, String imageTag, String profile, String baseImageDigest) { if (!forkRef?.trim()) { error('NAPCAT_FORK_REF is required before runtime image build.') } if (!upstreamReleaseCommit?.trim()) { error('UPSTREAM_RELEASE_COMMIT is required before runtime image build.') } if (!imageTag?.trim()) { error('QQBOT_NAPCAT_IMAGE_OVERRIDE is required before runtime image build.') } if (!profile?.trim()) { error('QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE is required before runtime image build.') } if (!baseImageDigest?.trim()?.contains('@sha256:')) { error('NAPCAT_BASE_IMAGE_DIGEST must be an immutable repo@sha256 digest.') } } /** * Ensures the promoted image tag and desktop profile cannot drift silently. * @param imageTag Runtime image tag, expected to end with the profile tag suffix. * @param profile Runtime desktop profile value passed to API promotion. * @return Nothing; aborts this pipeline through `error` when values drift. */ void requireImageProfileMatch(String imageTag, String profile) { if (!imageTag?.trim()?.endsWith(":${profile?.trim()}")) { error('QQBOT_NAPCAT_IMAGE_OVERRIDE tag suffix must match QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE.') } }