fix: 修复NapCat自动流水线闭环

This commit is contained in:
sunlei 2026-06-25 17:35:28 +08:00
parent 9eae9cc9a0
commit 65807e8ef3
5 changed files with 212 additions and 38 deletions

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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<void> {
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<void> {
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<void> {
"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<void> {
"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<void> {
"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}`);

View File

@ -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(

View File

@ -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.`,