diff --git a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile index a1f5e90..6882964 100644 --- a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile +++ b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile @@ -1,5 +1,5 @@ pipeline { - agent any + agent { label 'kt-node-agent' } options { buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '30')) @@ -25,6 +25,7 @@ pipeline { KT_WORKSPACE_ROOT = '/vol1/docker/kt-codex/workspace/KT' KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow' KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts' + KT_WORKFLOW_RUNNER_IMAGE = 'kt-jenkins-node-agent:current' } stages { @@ -32,12 +33,26 @@ pipeline { steps { sh ''' set -e - pnpm --dir "$KT_WORKFLOW_DIR" run napcat-runtime-release-readiness -- \ - --artifact-root "$KT_ARTIFACT_ROOT/napcat-runtime-release" \ - --fork-branch "$NAPCAT_FORK_REF" \ - --upstream-release-tag "$UPSTREAM_RELEASE_TAG" \ - --napcat-image-tag "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ - --profile "$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE" + docker run --rm \ + -e KT_WORKFLOW_DIR="$KT_WORKFLOW_DIR" \ + -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ + -e NAPCAT_FORK_REF="$NAPCAT_FORK_REF" \ + -e UPSTREAM_RELEASE_TAG="$UPSTREAM_RELEASE_TAG" \ + -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 \ + -v /vol1/docker/kt-codex:/vol1/docker/kt-codex \ + --entrypoint bash "$KT_WORKFLOW_RUNNER_IMAGE" -lc ' + set -e + source /vol1/docker/kt-codex/runtime/nvm/nvm.sh + corepack prepare pnpm@10.28.2 --activate + pnpm --dir "$KT_WORKFLOW_DIR" run napcat-runtime-release-readiness -- \ + --artifact-root "$KT_ARTIFACT_ROOT/napcat-runtime-release" \ + --fork-branch "$NAPCAT_FORK_REF" \ + --upstream-release-tag "$UPSTREAM_RELEASE_TAG" \ + --napcat-image-tag "$QQBOT_NAPCAT_IMAGE_OVERRIDE" \ + --profile "$QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE" + ' ''' } } @@ -93,11 +108,19 @@ pipeline { always { sh ''' set +e - rm -rf "$WORKSPACE/artifacts" - mkdir -p "$WORKSPACE/artifacts/napcat-runtime-release" - cp -a "$KT_ARTIFACT_ROOT/napcat-runtime-release/." "$WORKSPACE/artifacts/napcat-runtime-release/" 2>/dev/null || true + 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-release" + cp -a "$KT_ARTIFACT_ROOT/napcat-runtime-release/." "$WORKSPACE/artifacts/napcat-runtime-release/" 2>/dev/null || true + ' ''' - archiveArtifacts artifacts: 'artifacts/napcat-runtime-release/**/*', allowEmptyArchive: true, fingerprint: false + archiveArtifacts artifacts: 'artifacts/napcat-runtime-release/**', allowEmptyArchive: true, fingerprint: false } } } diff --git a/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile index a29052d..540ba5d 100644 --- a/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile +++ b/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile @@ -1,5 +1,5 @@ pipeline { - agent any + agent { label 'kt-node-agent' } options { buildDiscarder(logRotator(numToKeepStr: '30', artifactNumToKeepStr: '30')) @@ -17,6 +17,7 @@ pipeline { environment { KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow' KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts' + KT_WORKFLOW_RUNNER_IMAGE = 'kt-jenkins-node-agent:current' } stages { @@ -24,8 +25,19 @@ pipeline { steps { sh ''' set -e - pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ - --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run" + docker run --rm \ + -e KT_WORKFLOW_DIR="$KT_WORKFLOW_DIR" \ + -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ + -e CODEX_HOME=/vol1/docker/kt-codex/home/.codex \ + -e COREPACK_HOME=/vol1/docker/kt-codex/runtime/corepack \ + -v /vol1/docker/kt-codex:/vol1/docker/kt-codex \ + --entrypoint bash "$KT_WORKFLOW_RUNNER_IMAGE" -lc ' + set -e + source /vol1/docker/kt-codex/runtime/nvm/nvm.sh + corepack prepare pnpm@10.28.2 --activate + pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ + --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run" + ' ''' } } @@ -42,10 +54,21 @@ pipeline { } sh ''' set -e - pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ - --execute \ - --use-codex \ - --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex" + docker run --rm \ + -e KT_WORKFLOW_DIR="$KT_WORKFLOW_DIR" \ + -e KT_ARTIFACT_ROOT="$KT_ARTIFACT_ROOT" \ + -e CODEX_HOME=/vol1/docker/kt-codex/home/.codex \ + -e COREPACK_HOME=/vol1/docker/kt-codex/runtime/corepack \ + -v /vol1/docker/kt-codex:/vol1/docker/kt-codex \ + --entrypoint bash "$KT_WORKFLOW_RUNNER_IMAGE" -lc ' + set -e + source /vol1/docker/kt-codex/runtime/nvm/nvm.sh + corepack prepare pnpm@10.28.2 --activate + pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ + --execute \ + --use-codex \ + --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex" + ' ''' } } @@ -55,11 +78,19 @@ pipeline { always { sh ''' set +e - rm -rf "$WORKSPACE/artifacts" - mkdir -p "$WORKSPACE/artifacts/napcat-upstream-sync" - cp -a "$KT_ARTIFACT_ROOT/napcat-upstream-sync/." "$WORKSPACE/artifacts/napcat-upstream-sync/" 2>/dev/null || true + 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-upstream-sync" + cp -a "$KT_ARTIFACT_ROOT/napcat-upstream-sync/." "$WORKSPACE/artifacts/napcat-upstream-sync/" 2>/dev/null || true + ' ''' - archiveArtifacts artifacts: 'artifacts/napcat-upstream-sync/**/*', allowEmptyArchive: true, fingerprint: false + archiveArtifacts artifacts: 'artifacts/napcat-upstream-sync/**', allowEmptyArchive: true, fingerprint: false } } } diff --git a/src/selfTest.ts b/src/selfTest.ts index 0f9a121..0f54bdf 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -54,6 +54,7 @@ import { buildNasCodexBootstrapPlan, classifyNapcatUpstreamAudit, napcatAutomationDefaults, + shouldFailNapcatAutomationCli, } from "./tools/napcatAutomation.js"; import { registerTools } from "./registerTools.js"; import { @@ -119,6 +120,22 @@ export async function runSelfTest(): Promise { if (!napcatCli.execute || !napcatCli.useCodex) { throw new Error("NapCat upstream audit CLI parser self-check failed"); } + if ( + !shouldFailNapcatAutomationCli({ + classification: "blocked", + recommendedAction: "block", + }) || + shouldFailNapcatAutomationCli({ + classification: "manual-review", + recommendedAction: "request-human-review", + }) || + shouldFailNapcatAutomationCli({ + classification: "safe-candidate", + recommendedAction: "create-candidate-branch", + }) + ) { + throw new Error("NapCat automation CLI blocked gate self-check failed"); + } if ( napcatCli.artifactRoot !== "/vol1/docker/kt-codex/artifacts/napcat-upstream-sync" @@ -154,9 +171,17 @@ export async function runSelfTest(): Promise { throw new Error("NapCat auxiliary CLI dry-run default self-check failed"); } const napcatCandidateDefaults = buildNapcatSyncCandidateReview({}).context; - const napcatRuntimeDefaults = buildNapcatRuntimeReleaseReadiness({}).context; + const napcatRuntimeReadinessDefaults = buildNapcatRuntimeReleaseReadiness({}); + const napcatRuntimeDefaults = napcatRuntimeReadinessDefaults.context; const napcatRemoteDefaults = buildNapcatRemoteDevHandoff({}).context; const nasBootstrapDefaults = buildNasCodexBootstrapPlan({}).context; + for (const artifact of napcatRuntimeReadinessDefaults.artifacts) { + if (!existsSync(artifact.path)) { + throw new Error( + `NapCat runtime readiness artifact write self-check failed: ${artifact.path}`, + ); + } + } if ( napcatCandidateDefaults.candidateBranch !== napcatAutomationDefaults.candidateBranch || @@ -477,7 +502,7 @@ export async function runSelfTest(): Promise { "RUN_CODEX_WORKSPACE_WRITE_AUDIT", "buildDiscarder(logRotator", "disableConcurrentBuilds()", - "archiveArtifacts artifacts: 'artifacts/napcat-upstream-sync/**/*'", + "archiveArtifacts artifacts: 'artifacts/napcat-upstream-sync/**'", ]) { if (!upstreamSyncJenkinsTemplate.includes(expected)) { throw new Error(`NapCat upstream Jenkins template missing: ${expected}`); @@ -549,6 +574,62 @@ export async function runSelfTest(): Promise { "NapCat upstream Jenkins template unsafe default audit self-check failed", ); } + /** + * Verifies that a NapCat Jenkins template targets the NAS Jenkins node that can run shell work. + * @param template - Full Jenkinsfile text that Jenkins will execute. + * @param name - Human-readable template name for the self-test failure. + * @returns Nothing; throws when the template can stay queued on the exclusive agent topology. + */ + const assertNapCatJenkinsUsesNasAgent = ( + template: string, + name: string, + ): void => { + if (!template.includes("agent { label 'kt-node-agent' }")) { + throw new Error( + `${name} Jenkins template must target kt-node-agent instead of agent any`, + ); + } + }; + assertNapCatJenkinsUsesNasAgent( + upstreamSyncJenkinsTemplate, + "NapCat upstream", + ); + assertNapCatJenkinsUsesNasAgent( + runtimeReleaseJenkinsTemplate, + "NapCat runtime", + ); + /** + * Verifies that a NapCat Jenkins template runs ktWorkflow inside a helper container with NAS mounts. + * @param template - Full Jenkinsfile text that Jenkins will execute. + * @param name - Human-readable template name for the self-test failure. + * @returns Nothing; throws when the template assumes `/vol1` exists inside `kt-node-agent`. + */ + const assertNapCatJenkinsUsesWorkflowRunner = ( + template: string, + name: string, + ): void => { + for (const expected of [ + "KT_WORKFLOW_RUNNER_IMAGE", + "docker run --rm", + "/vol1/docker/kt-codex:/vol1/docker/kt-codex", + "COREPACK_HOME=/vol1/docker/kt-codex/runtime/corepack", + "corepack prepare pnpm@10.28.2 --activate", + ]) { + if (!template.includes(expected)) { + throw new Error( + `${name} Jenkins template must run ktWorkflow through the NAS helper container: ${expected}`, + ); + } + } + }; + assertNapCatJenkinsUsesWorkflowRunner( + upstreamSyncJenkinsTemplate, + "NapCat upstream", + ); + assertNapCatJenkinsUsesWorkflowRunner( + runtimeReleaseJenkinsTemplate, + "NapCat runtime", + ); for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) { if (upstreamSyncJenkinsTemplate.includes(forbidden)) { throw new Error( @@ -563,7 +644,7 @@ export async function runSelfTest(): Promise { "PROMOTE_KT_TEMPLATE_API_MAIN", "buildDiscarder(logRotator", "disableConcurrentBuilds()", - "archiveArtifacts artifacts: 'artifacts/napcat-runtime-release/**/*'", + "archiveArtifacts artifacts: 'artifacts/napcat-runtime-release/**'", ]) { if (!runtimeReleaseJenkinsTemplate.includes(expected)) { throw new Error(`NapCat runtime Jenkins template missing: ${expected}`); diff --git a/src/server.ts b/src/server.ts index bec1655..d01a93d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -29,8 +29,24 @@ import { buildNapcatSyncCandidateReview, buildNapcatUpstreamAudit, buildNasCodexBootstrapPlan, + shouldFailNapcatAutomationCli, } from './tools/napcatAutomation.js'; +/** + * Prints a NapCat automation result and turns blocked classifications into a failing CLI exit code. + * @param result - Structured NapCat automation result that must remain visible on stdout for Jenkins artifacts and manual diagnostics. + * @returns Nothing; writes JSON evidence and mutates `process.exitCode` when the result blocks safe continuation. + */ +function printNapcatAutomationCliResult(result: { + classification?: unknown; + recommendedAction?: unknown; +}): void { + console.log(JSON.stringify(result, null, 2)); + if (shouldFailNapcatAutomationCli(result)) { + process.exitCode = 1; + } +} + if (process.argv.includes('--cleanup-history')) { console.log(JSON.stringify(cleanupHistoryArtifacts(parseCliCleanupArgs(process.argv)), null, 2)); } else if (process.argv.includes('--global-review')) { @@ -56,8 +72,8 @@ if (process.argv.includes('--cleanup-history')) { ), ); } else if (process.argv.includes('--napcat-upstream-audit')) { - console.log( - JSON.stringify(buildNapcatUpstreamAudit(parseNapcatUpstreamAuditCliArgs(process.argv)), null, 2), + printNapcatAutomationCliResult( + buildNapcatUpstreamAudit(parseNapcatUpstreamAuditCliArgs(process.argv)), ); } else if (process.argv.includes('--napcat-sync-candidate-review')) { console.log( diff --git a/src/tools/napcatAutomation.ts b/src/tools/napcatAutomation.ts index 9344a75..db83440 100644 --- a/src/tools/napcatAutomation.ts +++ b/src/tools/napcatAutomation.ts @@ -117,6 +117,18 @@ export const napcatAutomationDefaults = { workspaceRoot: '/vol1/docker/kt-codex/workspace/KT', } as const; +/** + * Decides whether a NapCat automation CLI invocation must fail the hosting pipeline after printing JSON evidence. + * @param result - Structured ktWorkflow result from a NapCat automation command; only terminal block states represent infrastructure or safety failures. + * @returns True when Jenkins or another CLI caller must stop the pipeline instead of showing a green build with blocked evidence. + */ +export function shouldFailNapcatAutomationCli(result: { + classification?: unknown; + recommendedAction?: unknown; +}): boolean { + return result.classification === 'blocked' || result.recommendedAction === 'block'; +} + interface NapcatPromptSkeletonConfig { artifactSlug: string; commands: NapcatAuditCommand[]; @@ -144,9 +156,9 @@ function resolveNapcatSkeletonArtifactRoot( } /** - * Builds a deterministic dry-run prompt skeleton for Task 5 NapCat automation tools that are not allowed to mutate state yet. + * Builds a deterministic dry-run prompt skeleton for Task 5 NapCat automation tools that are not allowed to mutate release state yet. * @param config - Prompt name, context payload, read-only command hints, and caller execution intent. - * @returns A structured prompt/context/command package that never runs Codex or mutating release commands. + * @returns A structured prompt/context/command package; writes prompt and context artifacts for Jenkins archiving but never runs Codex or release mutation commands. */ function buildNapcatPromptSkeleton(config: NapcatPromptSkeletonConfig) { const artifactRoot = resolveNapcatSkeletonArtifactRoot( @@ -154,21 +166,32 @@ function buildNapcatPromptSkeleton(config: NapcatPromptSkeletonConfig) { config.artifactSlug, ); const requestedExecute = config.input.execute === true; + const promptText = loadNapcatAutomationPrompt(config.promptName).trim(); + const promptPath = joinNapcatArtifactPath( + artifactRoot, + `${config.artifactSlug}-prompt.md`, + ); + const contextPath = joinNapcatArtifactPath( + artifactRoot, + `${config.artifactSlug}-context.json`, + ); + + mkdirSync(artifactRoot, { recursive: true }); + writeFileSync(promptPath, promptText, 'utf8'); + writeFileSync( + contextPath, + JSON.stringify(config.context, null, 2), + 'utf8', + ); return { artifacts: [ { - path: joinNapcatArtifactPath( - artifactRoot, - `${config.artifactSlug}-prompt.md`, - ), + path: promptPath, type: 'markdown', }, { - path: joinNapcatArtifactPath( - artifactRoot, - `${config.artifactSlug}-context.json`, - ), + path: contextPath, type: 'json', }, ], @@ -184,7 +207,7 @@ function buildNapcatPromptSkeleton(config: NapcatPromptSkeletonConfig) { ], prompt: { name: config.promptName, - text: loadNapcatAutomationPrompt(config.promptName).trim(), + text: promptText, }, requestedExecute, summary: `${config.title} dry-run prompt skeleton generated.`,