From 114eef31955407bcd275f69b6819def54e69f353 Mon Sep 17 00:00:00 2001 From: sunlei Date: Fri, 26 Jun 2026 10:03:18 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E8=A1=A5=E9=BD=90NapCat=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E9=95=9C=E5=83=8F=E6=9E=84=E5=BB=BA=E4=B8=8B?= =?UTF-8?q?=E6=B8=B8Job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + README.md | 5 +- ...T-NapCatQQ-Runtime-Image-Build.Jenkinsfile | 190 ++++++++++++++++++ .../KT-NapCatQQ-Runtime-Release.Jenkinsfile | 6 + src/selfTest.ts | 89 ++++++++ 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile diff --git a/.gitignore b/.gitignore index 66b2bbd..d43741f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules/ dist/ .turbo/ +.kt-workspace/ *.log diff --git a/README.md b/README.md index 8763d40..0b0b13c 100644 --- a/README.md +++ b/README.md @@ -59,9 +59,10 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login - `ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile` 是 NapCatQQ 上游审计入口,默认只调用 ktWorkflow 的 `napcat-upstream-audit` dry-run,保留人工审查边界。 - Codex 上游审计必须同时设置 `RUN_CODEX_AUDIT=true` 并填写 `CONFIRM_CODEX_WORKSPACE_WRITE=RUN_CODEX_WORKSPACE_WRITE_AUDIT`;这是因为 Codex runner 使用 workspace-write sandbox,不能作为定时任务默认行为。 -- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion;下游 job 使用白名单 choice,避免任意 Jenkins job 名被传入。 +- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion;下游 job 使用白名单 choice,避免任意 Jenkins job 名被传入,并把 upstream release commit / NapCat base image digest 透传到镜像构建证据。 +- `ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile` 是运行时镜像构建下游,只从 NAS 本地裸仓库 clone 经过审查的 NapCatQQ fork ref,构建 `NapCat.Shell`,调用 API 仓库 `scripts/napcat-desktop-cn-stage-build.mjs` 生成 staged context,执行 `docker build` 和容器内 `/ci/napcat-desktop-cn/verify.sh`;它不触发 API promotion、不执行 `kubectl`、不合并或推送 Git。 - 生产 API promotion 除了 `PROMOTE_API_RUNTIME=true` 和明确 image/profile 参数外,还必须填写 `CONFIRM_API_PROMOTION_TARGET=PROMOTE_KT_TEMPLATE_API_MAIN`。 -- 两个模板默认使用 NAS 工作区 `/vol1/docker/kt-codex/workspace/KT`,运行证据落在 `/vol1/docker/kt-codex/artifacts` 下。 +- 三个模板默认使用 NAS 工作区 `/vol1/docker/kt-codex/workspace/KT`,运行证据落在 `/vol1/docker/kt-codex/artifacts` 下。 - 模板只保存入口和参数形状,不记录 Jenkins credential、Token、SSH key 或其他真实凭据。 ## MCP 客户端配置 diff --git a/ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile new file mode 100644 index 0000000..a642682 --- /dev/null +++ b/ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile @@ -0,0 +1,190 @@ +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' + 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 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 /var/run/docker.sock:/var/run/docker.sock \ + --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 + + rm -rf "$ARTIFACT_DIR" "$BUILD_WORKDIR" + mkdir -p "$ARTIFACT_DIR" "$BUILD_WORKDIR" + 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" "$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.') + } +} diff --git a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile index 6882964..e1ffc14 100644 --- a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile +++ b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile @@ -12,6 +12,8 @@ pipeline { parameters { string(name: 'NAPCAT_FORK_REF', defaultValue: 'main', description: 'Reviewed NapCatQQ fork branch, tag, or commit.') string(name: 'UPSTREAM_RELEASE_TAG', defaultValue: '', description: 'Reviewed upstream release tag, when applicable.') + string(name: 'UPSTREAM_RELEASE_COMMIT', defaultValue: '5c18a62530d87dbadf53d267002894faa6ca7e90', description: 'Reviewed upstream release commit used for runtime marker metadata.') + string(name: 'NAPCAT_BASE_IMAGE_DIGEST', defaultValue: 'mlikiowa/napcat-docker@sha256:9254ec12af101576c5eeb4910847abd1d219297bc6d9a35c52511e12500f0f45', description: 'Immutable NapCat base image digest used by the runtime image build.') string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', defaultValue: '', description: 'Explicit runtime image tag to build or promote, for example kt-napcat-desktop-cn:desktop-cn-v9.') string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', defaultValue: '', description: 'Explicit API desktop profile version override.') booleanParam(name: 'BUILD_RUNTIME_IMAGE', defaultValue: false, description: 'When true, trigger the reviewed Jenkins runtime image build/publish job.') @@ -38,6 +40,8 @@ pipeline { -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ -e NAPCAT_FORK_REF="$NAPCAT_FORK_REF" \ -e UPSTREAM_RELEASE_TAG="$UPSTREAM_RELEASE_TAG" \ + -e UPSTREAM_RELEASE_COMMIT="$UPSTREAM_RELEASE_COMMIT" \ + -e NAPCAT_BASE_IMAGE_DIGEST="$NAPCAT_BASE_IMAGE_DIGEST" \ -e QQBOT_NAPCAT_IMAGE_OVERRIDE="$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ -e QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE="$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE" \ -e COREPACK_HOME=/vol1/docker/kt-codex/runtime/corepack \ @@ -72,6 +76,8 @@ pipeline { parameters: [ string(name: 'NAPCAT_FORK_REF', value: params.NAPCAT_FORK_REF), string(name: 'UPSTREAM_RELEASE_TAG', value: params.UPSTREAM_RELEASE_TAG), + string(name: 'UPSTREAM_RELEASE_COMMIT', value: params.UPSTREAM_RELEASE_COMMIT), + string(name: 'NAPCAT_BASE_IMAGE_DIGEST', value: params.NAPCAT_BASE_IMAGE_DIGEST), string(name: 'QQBOT_NAPCAT_IMAGE_OVERRIDE', value: params.QQBOT_NAPCAT_IMAGE_OVERRIDE), string(name: 'QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE', value: params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE), string(name: 'KT_ARTIFACT_ROOT', value: "${env.KT_ARTIFACT_ROOT}/napcat-runtime-release") diff --git a/src/selfTest.ts b/src/selfTest.ts index 6fa1eca..db6b594 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -524,6 +524,12 @@ export async function runSelfTest(): Promise { ), "utf8", ).replace(/\r\n/g, "\n"); + const runtimeImageBuildJenkinsTemplate = readFileSync( + resolveInsideRoot( + "mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Runtime-Image-Build.Jenkinsfile", + ), + "utf8", + ).replace(/\r\n/g, "\n"); for (const expected of [ "RUN_CODEX_AUDIT", "CONFIRM_CODEX_WORKSPACE_WRITE", @@ -626,6 +632,10 @@ export async function runSelfTest(): Promise { runtimeReleaseJenkinsTemplate, "NapCat runtime", ); + assertNapCatJenkinsUsesNasAgent( + runtimeImageBuildJenkinsTemplate, + "NapCat runtime image build", + ); /** * Verifies that a NapCat Jenkins template runs ktWorkflow inside a helper container with NAS mounts. * @param template - Full Jenkinsfile text that Jenkins will execute. @@ -658,6 +668,44 @@ export async function runSelfTest(): Promise { runtimeReleaseJenkinsTemplate, "NapCat runtime", ); + assertNapCatJenkinsUsesWorkflowRunner( + runtimeImageBuildJenkinsTemplate, + "NapCat runtime image build", + ); + /** + * Ensures destructive Jenkins artifact cleanup is bounded by canonical NAS paths. + * @param template - Full Jenkinsfile text whose shell guard will run before `rm -rf`. + * @param name - Human-readable template name for the self-test failure. + * @returns Nothing; throws when the guard accepts raw `..` path prefixes. + */ + const assertNapCatJenkinsCanonicalArtifactRoot = ( + template: string, + name: string, + ): void => { + for (const expected of [ + '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"/*) ;;', + 'ARTIFACT_DIR="$ARTIFACT_ROOT_REAL/runtime-image-build"', + 'BUILD_WORKDIR="$ARTIFACT_ROOT_REAL/runtime-image-build-workspace/$BUILD_ID_SAFE"', + ]) { + if (!template.includes(expected)) { + throw new Error( + `${name} Jenkins template must canonicalize artifact roots before cleanup: ${expected}`, + ); + } + } + if (template.includes('case "$KT_ARTIFACT_ROOT" in')) { + throw new Error( + `${name} Jenkins template must not validate destructive cleanup paths with a raw prefix check`, + ); + } + }; + assertNapCatJenkinsCanonicalArtifactRoot( + runtimeImageBuildJenkinsTemplate, + "NapCat runtime image build", + ); for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) { if (upstreamSyncJenkinsTemplate.includes(forbidden)) { throw new Error( @@ -666,6 +714,8 @@ export async function runSelfTest(): Promise { } } for (const expected of [ + "UPSTREAM_RELEASE_COMMIT", + "NAPCAT_BASE_IMAGE_DIGEST", "choice(name: 'RUNTIME_BUILD_JOB'", "choice(name: 'API_PROMOTION_JOB'", "CONFIRM_API_PROMOTION_TARGET", @@ -748,6 +798,45 @@ export async function runSelfTest(): Promise { "NapCat runtime Jenkins API promotion confirmation must run before API build job", ); } + for (const expected of [ + "NAPCAT_FORK_REF", + "NAPCAT_GIT_REPOSITORY = '/vol1/docker/kt-codex/git/NapCatQQ.git'", + "QQBOT_NAPCAT_IMAGE_OVERRIDE", + "QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE", + "NAPCAT_BASE_IMAGE_DIGEST", + "requireRuntimeImageParameters(", + "requireImageProfileMatch(", + "-v /var/run/docker.sock:/var/run/docker.sock", + "git clone \"$NAPCAT_GIT_REPOSITORY\" \"$NAPCAT_ROOT\"", + "BUILD_WORKDIR=\"$ARTIFACT_ROOT_REAL/runtime-image-build-workspace/$BUILD_ID_SAFE\"", + "KT_ARTIFACT_ROOT must stay under /vol1/docker/kt-codex/artifacts", + "scripts/napcat-desktop-cn-stage-build.mjs", + "--napcat-root \"$NAPCAT_ROOT\"", + "--napcat-base-image-digest \"$NAPCAT_BASE_IMAGE_DIGEST\"", + "docker build", + "docker exec \"$VERIFY_NAME\" sh /ci/napcat-desktop-cn/verify.sh", + "archiveArtifacts artifacts: 'artifacts/napcat-runtime-image-build/**'", + ]) { + if (!runtimeImageBuildJenkinsTemplate.includes(expected)) { + throw new Error( + `NapCat runtime image build Jenkins template missing: ${expected}`, + ); + } + } + for (const forbidden of [ + "PROMOTE_API_RUNTIME", + "CONFIRM_API_PROMOTION_TARGET", + "kubectl", + "git push", + "git merge", + "/vol1/docker/kt-codex/workspace/KT/GitHub/NapCatQQ", + ]) { + if (runtimeImageBuildJenkinsTemplate.includes(forbidden)) { + throw new Error( + `NapCat runtime image build Jenkins template forbidden text leaked: ${forbidden}`, + ); + } + } const codexExecutionArtifactRoot = resolveInsideRoot( ".kt-workspace/test-artifacts/napcat-codex-runner-self-test", );