diff --git a/README.md b/README.md index 650fd69..77427a3 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,10 @@ pnpm run admin-login -- --url http://127.0.0.1:5999/#/auth/login ## Jenkins 入口模板 -- `ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile` 是 NapCatQQ 上游审计入口,只调用 ktWorkflow 的 `napcat-upstream-audit`,保留人工审查边界。 -- `ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile` 是运行时发布入口,先调用 `napcat-runtime-release-readiness`,再通过显式参数触发运行时镜像构建和 API promotion。 +- `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 名被传入。 +- 生产 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` 下。 - 模板只保存入口和参数形状,不记录 Jenkins credential、Token、SSH key 或其他真实凭据。 diff --git a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile index dc9f006..86be13b 100644 --- a/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile +++ b/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile @@ -13,9 +13,10 @@ pipeline { 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.') - string(name: 'RUNTIME_BUILD_JOB', defaultValue: 'KT-NapCatQQ/Runtime-Image-Build', description: 'Jenkins job that performs the reviewed Docker runtime build and publish.') + choice(name: 'RUNTIME_BUILD_JOB', choices: ['KT-NapCatQQ/Runtime-Image-Build'], description: 'Whitelisted Jenkins job that performs the reviewed runtime image build and publish.') booleanParam(name: 'PROMOTE_API_RUNTIME', defaultValue: false, description: 'When true, trigger API promotion with explicit image/profile overrides.') - string(name: 'API_PROMOTION_JOB', defaultValue: 'KT-Template/KT-Template-API/main', description: 'API Jenkins job that accepts NapCat runtime override parameters.') + choice(name: 'API_PROMOTION_JOB', choices: ['KT-Template/KT-Template-API/main'], description: 'Whitelisted API Jenkins job that accepts NapCat runtime override parameters.') + string(name: 'CONFIRM_API_PROMOTION_TARGET', defaultValue: '', description: 'Type PROMOTE_KT_TEMPLATE_API_MAIN before triggering the production API promotion job.') } environment { @@ -29,7 +30,7 @@ pipeline { steps { sh ''' set -e - pnpm --dir /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow run napcat-runtime-release-readiness -- \ + 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" \ @@ -45,6 +46,9 @@ pipeline { } steps { script { + if (params.CONFIRM_API_PROMOTION_TARGET != 'PROMOTE_KT_TEMPLATE_API_MAIN') { + error('CONFIRM_API_PROMOTION_TARGET must equal PROMOTE_KT_TEMPLATE_API_MAIN before API promotion runs.') + } requireRuntimeOverrides( params.QQBOT_NAPCAT_IMAGE_OVERRIDE, params.QQBOT_NAPCAT_DESKTOP_PROFILE_VERSION_OVERRIDE diff --git a/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile b/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile index 552dc04..bd13476 100644 --- a/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile +++ b/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile @@ -7,15 +7,43 @@ pipeline { timeout(time: 30, unit: 'MINUTES') } + parameters { + booleanParam(name: 'RUN_CODEX_AUDIT', defaultValue: false, description: 'When true, allow the optional Codex workspace-write audit after the default dry-run audit.') + string(name: 'CONFIRM_CODEX_WORKSPACE_WRITE', defaultValue: '', description: 'Type RUN_CODEX_WORKSPACE_WRITE_AUDIT to confirm Codex may run with a workspace-write sandbox.') + } + + environment { + KT_WORKFLOW_DIR = '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow' + KT_ARTIFACT_ROOT = '/vol1/docker/kt-codex/artifacts' + } + stages { - stage('ktWorkflow upstream audit') { + stage('ktWorkflow upstream audit dry-run') { steps { sh ''' set -e - pnpm --dir /vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow run napcat-upstream-audit -- \ + pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ + --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/dry-run" + ''' + } + } + + stage('ktWorkflow upstream audit with Codex') { + when { + expression { return params.RUN_CODEX_AUDIT } + } + steps { + script { + if (params.CONFIRM_CODEX_WORKSPACE_WRITE != 'RUN_CODEX_WORKSPACE_WRITE_AUDIT') { + error('CONFIRM_CODEX_WORKSPACE_WRITE must equal RUN_CODEX_WORKSPACE_WRITE_AUDIT before Codex workspace-write audit runs.') + } + } + sh ''' + set -e + pnpm --dir "$KT_WORKFLOW_DIR" run napcat-upstream-audit -- \ --execute \ --use-codex \ - --artifact-root /vol1/docker/kt-codex/artifacts/napcat-upstream-sync + --artifact-root "$KT_ARTIFACT_ROOT/napcat-upstream-sync/codex" ''' } } diff --git a/src/selfTest.ts b/src/selfTest.ts index bb383c0..3f12f9f 100644 --- a/src/selfTest.ts +++ b/src/selfTest.ts @@ -449,6 +449,81 @@ export async function runSelfTest(): Promise { if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) { throw new Error("NapCat Codex runner mutation guard self-check failed"); } + const upstreamSyncJenkinsTemplate = readFileSync( + resolveInsideRoot( + "mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Upstream-Sync.Jenkinsfile", + ), + "utf8", + ); + const runtimeReleaseJenkinsTemplate = readFileSync( + resolveInsideRoot( + "mcp/ktWorkflow/ci/jenkins/KT-NapCatQQ-Runtime-Release.Jenkinsfile", + ), + "utf8", + ); + for (const expected of [ + "RUN_CODEX_AUDIT", + "CONFIRM_CODEX_WORKSPACE_WRITE", + "RUN_CODEX_WORKSPACE_WRITE_AUDIT", + ]) { + if (!upstreamSyncJenkinsTemplate.includes(expected)) { + throw new Error(`NapCat upstream Jenkins template missing: ${expected}`); + } + } + const defaultUpstreamAuditIndex = upstreamSyncJenkinsTemplate.indexOf( + "run napcat-upstream-audit --", + ); + const firstUpstreamExecuteIndex = + upstreamSyncJenkinsTemplate.indexOf("--execute"); + if ( + defaultUpstreamAuditIndex === -1 || + (firstUpstreamExecuteIndex !== -1 && + defaultUpstreamAuditIndex > firstUpstreamExecuteIndex) + ) { + throw new Error( + "NapCat upstream Jenkins template must run dry-run audit before execute", + ); + } + const defaultUpstreamAuditCall = upstreamSyncJenkinsTemplate.slice( + defaultUpstreamAuditIndex, + firstUpstreamExecuteIndex === -1 + ? upstreamSyncJenkinsTemplate.length + : firstUpstreamExecuteIndex, + ); + if (defaultUpstreamAuditCall.includes("--use-codex")) { + throw new Error( + "NapCat upstream Jenkins template default audit must not use Codex", + ); + } + for (const forbidden of ["git merge", "git push", "docker build", "kubectl"]) { + if (upstreamSyncJenkinsTemplate.includes(forbidden)) { + throw new Error( + `NapCat upstream Jenkins template forbidden command leaked: ${forbidden}`, + ); + } + } + for (const expected of [ + "choice(name: 'RUNTIME_BUILD_JOB'", + "choice(name: 'API_PROMOTION_JOB'", + "CONFIRM_API_PROMOTION_TARGET", + "PROMOTE_KT_TEMPLATE_API_MAIN", + ]) { + if (!runtimeReleaseJenkinsTemplate.includes(expected)) { + throw new Error(`NapCat runtime Jenkins template missing: ${expected}`); + } + } + for (const forbidden of [ + "string(name: 'RUNTIME_BUILD_JOB'", + "string(name: 'API_PROMOTION_JOB'", + "git merge", + "git push", + ]) { + if (runtimeReleaseJenkinsTemplate.includes(forbidden)) { + throw new Error( + `NapCat runtime Jenkins template forbidden text leaked: ${forbidden}`, + ); + } + } const codexExecutionArtifactRoot = resolveInsideRoot( ".kt-workspace/test-artifacts/napcat-codex-runner-self-test", );