ktworkflow-mcp/ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile

213 lines
10 KiB
Plaintext

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.')
}
}