diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 8cab2312..9cffa39d 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -20,8 +20,9 @@ - 背景:`Genarrative-Stdb-Module-Build` 在 Windows Jenkins 本地环境里调用裸 `powershell` step 时触发 `CreateProcess error=5, 拒绝访问`,而 `powershell.exe` 本体与 workspace ACL 都正常。 - 决策:Windows Jenkins 上凡是需要执行 PowerShell 逻辑的流水线,优先通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`,不要再依赖 Jenkins `powershell` step 的隐式启动器。 +- 追加决策:`Genarrative-Stdb-Module-Build` 的 Checkout 逻辑应复用 Jenkins GitSCM 已完成的工作区状态。`COMMIT_HASH` 为空或已与当前 `HEAD` 一致时,不再额外执行 `git clean` / `git checkout`;只有需要切到指定且不同的 commit 时才补 fetch、校验和切换,避免在 Windows workspace 里二次清理触发权限拒绝。 - 影响范围:`jenkins/Jenkinsfile.production-stdb-module-build` 及后续所有同类 Windows 构建流水线。 -- 验证方式:Jenkins 日志中应能看到 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,并继续执行 checkout / build;不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"`。 +- 验证方式:Jenkins 日志中应能看到 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,Checkout 阶段会打印当前 `HEAD` 与请求 commit,并在 `COMMIT_HASH` 为空或一致时直接继续;不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"` 或重复 `git clean` 的退出码 5。 - 关联文档:`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`.hermes/shared-memory/pitfalls.md`。 ## 2026-05-17 容器化方案只作为隔离压测与预发模拟路径 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index fff6779b..2b8ccc05 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -947,7 +947,7 @@ ## Windows Jenkins `powershell` step 在 Stdb module 构建里曾触发 CreateProcess error=5 - 现象:`Genarrative-Stdb-Module-Build` 在 Windows Jenkins 节点上报 `java.io.IOException: Cannot run program "powershell" (in directory "C:\\Users\\DSK\\.jenkins-local\\workspace\\Genarrative-Stdb-Module-Build"): CreateProcess error=5, 拒绝访问。`;日志里能看到 `durable-task` 已写出 `powershellWrapper.ps1`,但在真正启动裸 `powershell` 子进程时失败。 -- 原因:Jenkins durable-task 的 `powershell` step 依赖一个隐式命令解析/启动路径,在这台 Windows 本地 Jenkins 环境里会被拒绝。`powershell.exe` 本体和 workspace ACL 都是正常的,问题出在 Jenkins step 的启动方式,而不是 PowerShell 脚本内容。 -- 处理:把 `jenkins/Jenkinsfile.production-stdb-module-build` 的 `Checkout` 和 `Build Stdb Module` 两处 `powershell` step 收口成 `runWindowsPowerShell(...)` helper,先用 `writeFile` 写出临时 `.ps1`,再通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`。`Checkout` 阶段保留 `.jenkins-*.ps1`,并用 `git clean -e ".jenkins-*.ps1"` 避免被清掉。 -- 验证:检查 Jenkins build log 中是否出现 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,以及后续 `Checkout` / `Build Stdb Module` 是否继续执行;同时确认 `builds//log` 不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"`。 +- 原因:Jenkins durable-task 的 `powershell` step 依赖一个隐式命令解析/启动路径,在这台 Windows 本地 Jenkins 环境里会被拒绝。`powershell.exe` 本体和 workspace ACL 都是正常的,问题出在 Jenkins step 的启动方式,而不是 PowerShell 脚本内容。修复后若日志能打印 `[jenkins-powershell] exe:`,但随后仅报 `拒绝访问` / `script returned exit code 5`,通常已经不是 PowerShell 启动失败,而是 Checkout 脚本内部命令在 Windows workspace 里触发权限拒绝。 +- 处理:把 `jenkins/Jenkinsfile.production-stdb-module-build` 的 `Checkout` 和 `Build Stdb Module` 两处 `powershell` step 收口成 `runWindowsPowerShell(...)` helper,先用 `writeFile` 写出临时 `.ps1`,再通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe -NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File ...`。Checkout 阶段优先复用 Jenkins GitSCM 已完成的工作区结果;`COMMIT_HASH` 为空或已经等于当前 `HEAD` 时不再重复 `git fetch` / `git checkout` / `git clean`,只有确实要切到另一个指定 commit 时才补 fetch、归属校验和 checkout。 +- 验证:检查 Jenkins build log 中是否出现 `[jenkins-powershell] user:` 和 `[jenkins-powershell] exe:`,以及 `[stdb-checkout] current HEAD:`。上游 Full Build 传下来的 `COMMIT_HASH` 若已等于当前 GitSCM checkout,日志应显示 `requested commit already matches Jenkins GitSCM checkout` 并继续进入构建阶段;同时确认 `builds//log` 不再停在 `PipelineNodeTreeScanner... Cannot run program "powershell"` 或 Checkout 内部 exit code 5。 - 关联:`jenkins/Jenkinsfile.production-stdb-module-build`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 34368a39..390e84f3 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -149,7 +149,7 @@ Nginx 负责站点和反向代理 Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分 ``` -Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该流水线需要执行 PowerShell 逻辑时,统一通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`,不要直接使用 Jenkins `powershell` step;本地 Jenkins durable-task 曾在 `Genarrative-Stdb-Module-Build` workspace 中启动裸 `powershell` 时触发 `CreateProcess error=5, 拒绝访问`。排查时先看对应 build log、`@tmp/durable-*` 下的 `powershellWrapper.ps1`,以及日志中的 `[jenkins-powershell] user/exe`。 +Windows Stdb module 构建流水线运行在 Jenkins `windows` 节点上。该流水线需要执行 PowerShell 逻辑时,统一通过 `bat` 显式调用 `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`,不要直接使用 Jenkins `powershell` step;本地 Jenkins durable-task 曾在 `Genarrative-Stdb-Module-Build` workspace 中启动裸 `powershell` 时触发 `CreateProcess error=5, 拒绝访问`。Checkout 阶段要优先复用 Jenkins GitSCM 已经完成的结果:`COMMIT_HASH` 为空或与当前 `HEAD` 一致时,不要再额外 `git clean` / `git checkout`,只在确实需要切到别的指定 commit 时才补 fetch、校验和切换。排查时先看对应 build log、`@tmp/durable-*` 下的 `powershellWrapper.ps1`,以及日志中的 `[jenkins-powershell] user/exe`。 生产环境变量模板:`deploy/env/api-server.env.example`。真实密钥只放服务器,不提交 Git,不写入文档示例。 diff --git a/jenkins/Jenkinsfile.production-stdb-module-build b/jenkins/Jenkinsfile.production-stdb-module-build index cf1c2efc..058967ab 100644 --- a/jenkins/Jenkinsfile.production-stdb-module-build +++ b/jenkins/Jenkinsfile.production-stdb-module-build @@ -70,13 +70,85 @@ pipeline { $sourceBranch = if ($env:SOURCE_BRANCH) { $env:SOURCE_BRANCH } else { 'master' } $commitHash = if ($env:COMMIT_HASH) { $env:COMMIT_HASH } else { '' } $gitRemoteUrl = if ($env:GIT_REMOTE_URL) { $env:GIT_REMOTE_URL } else { 'https://git.genarrative.world/GenarrativeAI/Genarrative.git' } - git fetch --no-tags --prune --depth=1 $gitRemoteUrl "+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}" - if ($commitHash) { - git checkout --force $commitHash - } else { - git checkout --force "origin/$sourceBranch" + + function Invoke-GitCommand { + param( + [string]$Label, + [string[]]$Arguments + ) + + Write-Host "[stdb-checkout] $Label" + & git @Arguments + $exitCode = $LASTEXITCODE + if ($exitCode -ne 0) { + throw "[stdb-checkout] $Label failed with exit code $exitCode" + } } - git clean -ffdx -e ".jenkins-*.ps1" + + Write-Host "[stdb-checkout] sourceBranch: $sourceBranch" + Write-Host "[stdb-checkout] remote: $gitRemoteUrl" + $currentCommit = (git rev-parse HEAD).Trim() + if ($LASTEXITCODE -ne 0 -or -not $currentCommit) { + throw '[stdb-checkout] cannot resolve current HEAD' + } + Write-Host "[stdb-checkout] current HEAD: $currentCommit" + + if ($commitHash) { + Write-Host "[stdb-checkout] requested commit: $commitHash" + $resolvedCommit = (git rev-parse --verify "${commitHash}^{commit}" 2>$null).Trim() + if ($LASTEXITCODE -eq 0 -and $resolvedCommit -eq $currentCommit) { + Write-Host '[stdb-checkout] requested commit already matches Jenkins GitSCM checkout' + } else { + Invoke-GitCommand -Label 'fetch source branch history' -Arguments @( + 'fetch', + '--no-tags', + '--prune', + $gitRemoteUrl, + "+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}" + ) + $isShallowRepository = (git rev-parse --is-shallow-repository 2>$null).Trim() + if ($LASTEXITCODE -ne 0) { + throw '[stdb-checkout] cannot determine whether repository is shallow' + } + if ($isShallowRepository -eq 'true') { + Invoke-GitCommand -Label 'deepen source branch history' -Arguments @( + 'fetch', + '--unshallow', + '--no-tags', + $gitRemoteUrl, + "+refs/heads/${sourceBranch}:refs/remotes/origin/${sourceBranch}" + ) + } + Invoke-GitCommand -Label 'validate source branch ref' -Arguments @( + 'cat-file', + '-e', + "refs/remotes/origin/${sourceBranch}^{commit}" + ) + Invoke-GitCommand -Label 'validate requested commit' -Arguments @( + 'cat-file', + '-e', + "${commitHash}^{commit}" + ) + $resolvedCommit = (git rev-parse --verify "${commitHash}^{commit}").Trim() + if ($LASTEXITCODE -ne 0 -or -not $resolvedCommit) { + throw "[stdb-checkout] cannot resolve requested commit: $commitHash" + } + Invoke-GitCommand -Label 'validate requested commit belongs to branch' -Arguments @( + 'merge-base', + '--is-ancestor', + $resolvedCommit, + "refs/remotes/origin/${sourceBranch}" + ) + Invoke-GitCommand -Label "checkout commit $resolvedCommit" -Arguments @( + 'checkout', + '--force', + $resolvedCommit + ) + } + } else { + Write-Host "[stdb-checkout] COMMIT_HASH empty, reusing Jenkins GitSCM checkout result" + } + $resolvedCommit = (git rev-parse HEAD).Trim() $utf8NoBom = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText((Join-Path (Get-Location) '.jenkins-source-commit'), "$resolvedCommit`n", $utf8NoBom)