From 6befa3c33a2b6dff31509fc42272c2f468582d40 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 17:29:25 +0800 Subject: [PATCH 1/8] =?UTF-8?q?=E8=A1=A5=E5=85=A8=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E6=B5=81=E6=B0=B4=E7=BA=BF=E9=A6=96=E6=AC=A1=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 Jenkins Job 配置中预声明部署参数 增加 buildWithParameters 首次触发合同检查 --- jenkins/preview-deployer-job-config.xml | 32 ++++++++++++++++++++++++- scripts/check-preview-deployer.mjs | 5 ++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/jenkins/preview-deployer-job-config.xml b/jenkins/preview-deployer-job-config.xml index a2317950f..a0cbd4247 100644 --- a/jenkins/preview-deployer-job-config.xml +++ b/jenkins/preview-deployer-job-config.xml @@ -5,7 +5,37 @@ false - + + + ACTION + 部署、查询状态或卸载分支预览容器 + + + DEPLOY + STATUS + UNINSTALL + + + + + SOURCE_BRANCH + 源码分支;DEPLOY 和 UNINSTALL 必填 + + true + + + COMMIT_HASH + 可选;必须是 SOURCE_BRANCH 历史中的 commit + + true + + + DEPLOYMENT_ID + 预览实例稳定 ID + + true + + diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index 230052052..32d1b0497 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -132,6 +132,11 @@ assertIncludes( 'jenkins/Jenkinsfile.preview-deployer', 'Jenkins Job 必须从受保护 master 读取固定流水线。', ); +assertIncludes( + jobConfig, + '', + 'Jenkins Job 首次触发前必须已经声明 buildWithParameters 参数。', +); assertIncludes( nginx, 'location ^~ /api/preview-deployer/', From 7400c706f8113c57cd6db3540e5273c59acc0f1a Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 17:39:07 +0800 Subject: [PATCH 2/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E6=B5=81=E6=B0=B4=E7=BA=BF=E7=A9=BA=E5=8F=82=E6=95=B0=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在严格 shell 模式下归一化首次触发的空参数 避免 STATUS 与未指定 commit 的请求误报未定义变量 --- jenkins/Jenkinsfile.preview-deployer | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 6591da10f..3f4e5961e 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -47,6 +47,10 @@ pipeline { steps { sh ''' set -euo pipefail + ACTION="${ACTION:-}" + SOURCE_BRANCH="${SOURCE_BRANCH:-}" + COMMIT_HASH="${COMMIT_HASH:-}" + DEPLOYMENT_ID="${DEPLOYMENT_ID:-}" case "${ACTION}" in DEPLOY|STATUS|UNINSTALL) ;; *) exit 1 ;; esac if [ -n "${SOURCE_BRANCH}" ]; then git check-ref-format --branch "${SOURCE_BRANCH}" >/dev/null From d2366bc40a417a0b762303e03db883b296fb2d3d Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 17:59:27 +0800 Subject: [PATCH 3/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E5=8F=AF=E9=80=89=E6=8F=90=E4=BA=A4=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 Checkout 阶段在严格 shell 下展开未定义 COMMIT_HASH 的失败 增加预览流水线参数归一化防回归门禁 同步预览部署参数合同与项目决策文档 --- .../shared-memory/decision-log.md | 6 +++++ ...Jenkins容器预览部署控制面技术方案-2026-08-15.md | 2 ++ jenkins/Jenkinsfile.preview-deployer | 10 +++++--- scripts/check-preview-deployer.mjs | 25 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index bc671a867..1394bf4f0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14161,3 +14161,9 @@ - 实例与端口:分支规范化后形成稳定 `deploymentId`,同一分支换 commit 复用实例和 Web 端口;不同分支使用独立 Compose project。Web 端口在全局文件锁内从 `8400..8499` 分配,状态表与宿主监听同时空闲才可占用,卸载后释放。SpacetimeDB 与 OTLP 不映射宿主端口,Jenkins 通过受控 Compose 网络发布模块;页面只展示 Web 内网地址。 - 来源与卸载:部署只接受 `SOURCE_BRANCH` 和可选 `COMMIT_HASH`,Jenkins 必须证明 commit 属于目标分支。卸载只接受受控状态中存在的 `deploymentId`,客户端不能传 Jenkins URL、Job、Compose project、容器名或端口。状态通过固定 `preview-result.json` artifact 返回,不解析或向浏览器暴露完整 console。 - 关联文档:`docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 2026-08-15 Jenkins 预览部署可选 commit 的跨 shell 参数合同 + +- 决策:`COMMIT_HASH` 在 Preview Deployer 全链路保持可选。控制服务正常请求显式传空字符串;Jenkins 流水线同时兼容首次参数未物化和旧构建重跑,在每个独立 `sh` 中使用 `${COMMIT_HASH:-}` 归一化,禁止依赖前一 shell 的局部变量。 +- `DEPLOY` 缺少 `SOURCE_BRANCH` 必须失败关闭;分支明确而 commit 为空时才表示选择该目标分支的远端最新提交,仍统一经过 `scripts/jenkins-checkout-source.sh`,不得改用控制器 `master`、忽略 commit 归属或关闭严格 shell。 +- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 diff --git a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md index 23436d15c..2e4c04978 100644 --- a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md +++ b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md @@ -56,6 +56,8 @@ SpacetimeDB 与 OTLP 不映射宿主端口;Jenkins 通过受控 Compose 网络 分支名只允许数字、字母、点、下划线、短横线和斜杠,不允许首尾斜杠或连续点号。Jenkins 必须复用 `scripts/jenkins-checkout-source.sh`,验证 commit 真实属于目标远端分支;SPA 和代理服务的输入校验不能代替流水线校验。 +`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。 + 每次运行归档 `preview-result.json`,至少包含: ```json diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 3f4e5961e..7ab5b038a 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -52,6 +52,10 @@ pipeline { COMMIT_HASH="${COMMIT_HASH:-}" DEPLOYMENT_ID="${DEPLOYMENT_ID:-}" case "${ACTION}" in DEPLOY|STATUS|UNINSTALL) ;; *) exit 1 ;; esac + if [ "${ACTION}" = 'DEPLOY' ] && [ -z "${SOURCE_BRANCH}" ]; then + echo 'DEPLOY 必须提供 SOURCE_BRANCH。' >&2 + exit 1 + fi if [ -n "${SOURCE_BRANCH}" ]; then git check-ref-format --branch "${SOURCE_BRANCH}" >/dev/null fi @@ -93,9 +97,9 @@ pipeline { withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) { sh ''' set -euo pipefail - SOURCE_BRANCH="${SOURCE_BRANCH}" \ - COMMIT_HASH="${COMMIT_HASH}" \ - GIT_REMOTE_URL="${GIT_REMOTE_URL}" \ + SOURCE_BRANCH="${SOURCE_BRANCH:-}" \ + COMMIT_HASH="${COMMIT_HASH:-}" \ + GIT_REMOTE_URL="${GIT_REMOTE_URL:-}" \ SOURCE_COMMIT_FILE=".jenkins-source-commit" \ GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \ GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index 32d1b0497..e3ce90a2f 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -55,6 +55,31 @@ assertIncludes( "string(name: 'DEPLOYMENT_ID'", 'Jenkins 必须暴露 DEPLOYMENT_ID。', ); +assertIncludes( + jenkinsfile, + 'SOURCE_BRANCH="${SOURCE_BRANCH:-}"', + 'Jenkins 目标源码 checkout 必须兼容旧构建缺少 SOURCE_BRANCH 环境变量。', +); +assertIncludes( + jenkinsfile, + 'COMMIT_HASH="${COMMIT_HASH:-}"', + 'Jenkins 目标源码 checkout 必须兼容可选 COMMIT_HASH 未物化。', +); +assertIncludes( + jenkinsfile, + 'GIT_REMOTE_URL="${GIT_REMOTE_URL:-}"', + 'Jenkins 目标源码 checkout 必须在严格 shell 下安全读取固定 Git 地址。', +); +assertExcludes( + jenkinsfile, + 'COMMIT_HASH="${COMMIT_HASH}"', + 'Jenkins 不得在 set -u 下严格展开可选 COMMIT_HASH。', +); +assertIncludes( + jenkinsfile, + `if [ "${'${ACTION}'}" = 'DEPLOY' ] && [ -z "${'${SOURCE_BRANCH}'}" ]; then`, + 'Jenkins DEPLOY 缺少 SOURCE_BRANCH 时必须失败关闭。', +); assertIncludes( jenkinsfile, "archiveArtifacts artifacts: 'preview-result.json'", From 7fc739fad0b082844b337c14a31eace2492b2c5e Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 18:03:49 +0800 Subject: [PATCH 4/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=E6=89=A7=E8=A1=8C=E6=96=B9?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 通过 bash 显式调用 Jenkins 源码检出脚本 增加脚本执行方式防回归门禁 同步预览部署执行合同文档 --- docs/project-memory/shared-memory/decision-log.md | 3 ++- ...开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md | 2 +- jenkins/Jenkinsfile.preview-deployer | 2 +- scripts/check-preview-deployer.mjs | 5 +++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 1394bf4f0..d740cf677 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14166,4 +14166,5 @@ - 决策:`COMMIT_HASH` 在 Preview Deployer 全链路保持可选。控制服务正常请求显式传空字符串;Jenkins 流水线同时兼容首次参数未物化和旧构建重跑,在每个独立 `sh` 中使用 `${COMMIT_HASH:-}` 归一化,禁止依赖前一 shell 的局部变量。 - `DEPLOY` 缺少 `SOURCE_BRANCH` 必须失败关闭;分支明确而 commit 为空时才表示选择该目标分支的远端最新提交,仍统一经过 `scripts/jenkins-checkout-source.sh`,不得改用控制器 `master`、忽略 commit 归属或关闭严格 shell。 -- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 +- 调用方式:Preview Deployer 通过 `bash` 显式执行 `scripts/jenkins-checkout-source.sh`,不依赖仓库文件可执行位;GitSCM checkout、归档或文件模式漂移不得让合法部署停在 exit code 126。 +- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`,并要求 helper 经 `bash` 调用;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 diff --git a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md index 2e4c04978..a81e45436 100644 --- a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md +++ b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md @@ -56,7 +56,7 @@ SpacetimeDB 与 OTLP 不映射宿主端口;Jenkins 通过受控 Compose 网络 分支名只允许数字、字母、点、下划线、短横线和斜杠,不允许首尾斜杠或连续点号。Jenkins 必须复用 `scripts/jenkins-checkout-source.sh`,验证 commit 真实属于目标远端分支;SPA 和代理服务的输入校验不能代替流水线校验。 -`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。 +`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。流水线通过 `bash` 显式调用该 helper,不依赖 Git checkout 后的文件可执行位。 每次运行归档 `preview-result.json`,至少包含: diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 7ab5b038a..5e0691726 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -103,7 +103,7 @@ pipeline { SOURCE_COMMIT_FILE=".jenkins-source-commit" \ GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \ GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ - "${WORKSPACE}/scripts/jenkins-checkout-source.sh" + bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh" ''' } } diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index e3ce90a2f..0d8bbce18 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -70,6 +70,11 @@ assertIncludes( 'GIT_REMOTE_URL="${GIT_REMOTE_URL:-}"', 'Jenkins 目标源码 checkout 必须在严格 shell 下安全读取固定 Git 地址。', ); +assertIncludes( + jenkinsfile, + 'bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh"', + 'Jenkins 必须通过 bash 调用 checkout helper,不能依赖仓库文件可执行位。', +); assertExcludes( jenkinsfile, 'COMMIT_HASH="${COMMIT_HASH}"', From 5e7ff450b68293e886bff994620884aa364654bc Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 18:09:04 +0800 Subject: [PATCH 5/8] =?UTF-8?q?Revert=20"=E4=BF=AE=E5=A4=8D=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E9=83=A8=E7=BD=B2=E8=84=9A=E6=9C=AC=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=96=B9=E5=BC=8F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7fc739fad0b082844b337c14a31eace2492b2c5e. --- docs/project-memory/shared-memory/decision-log.md | 3 +-- ...开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md | 2 +- jenkins/Jenkinsfile.preview-deployer | 2 +- scripts/check-preview-deployer.mjs | 5 ----- 4 files changed, 3 insertions(+), 9 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index d740cf677..1394bf4f0 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14166,5 +14166,4 @@ - 决策:`COMMIT_HASH` 在 Preview Deployer 全链路保持可选。控制服务正常请求显式传空字符串;Jenkins 流水线同时兼容首次参数未物化和旧构建重跑,在每个独立 `sh` 中使用 `${COMMIT_HASH:-}` 归一化,禁止依赖前一 shell 的局部变量。 - `DEPLOY` 缺少 `SOURCE_BRANCH` 必须失败关闭;分支明确而 commit 为空时才表示选择该目标分支的远端最新提交,仍统一经过 `scripts/jenkins-checkout-source.sh`,不得改用控制器 `master`、忽略 commit 归属或关闭严格 shell。 -- 调用方式:Preview Deployer 通过 `bash` 显式执行 `scripts/jenkins-checkout-source.sh`,不依赖仓库文件可执行位;GitSCM checkout、归档或文件模式漂移不得让合法部署停在 exit code 126。 -- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`,并要求 helper 经 `bash` 调用;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 +- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 diff --git a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md index a81e45436..2e4c04978 100644 --- a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md +++ b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md @@ -56,7 +56,7 @@ SpacetimeDB 与 OTLP 不映射宿主端口;Jenkins 通过受控 Compose 网络 分支名只允许数字、字母、点、下划线、短横线和斜杠,不允许首尾斜杠或连续点号。Jenkins 必须复用 `scripts/jenkins-checkout-source.sh`,验证 commit 真实属于目标远端分支;SPA 和代理服务的输入校验不能代替流水线校验。 -`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。流水线通过 `bash` 显式调用该 helper,不依赖 Git checkout 后的文件可执行位。 +`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。 每次运行归档 `preview-result.json`,至少包含: diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 5e0691726..7ab5b038a 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -103,7 +103,7 @@ pipeline { SOURCE_COMMIT_FILE=".jenkins-source-commit" \ GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \ GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ - bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh" + "${WORKSPACE}/scripts/jenkins-checkout-source.sh" ''' } } diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index 0d8bbce18..e3ce90a2f 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -70,11 +70,6 @@ assertIncludes( 'GIT_REMOTE_URL="${GIT_REMOTE_URL:-}"', 'Jenkins 目标源码 checkout 必须在严格 shell 下安全读取固定 Git 地址。', ); -assertIncludes( - jenkinsfile, - 'bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh"', - 'Jenkins 必须通过 bash 调用 checkout helper,不能依赖仓库文件可执行位。', -); assertExcludes( jenkinsfile, 'COMMIT_HASH="${COMMIT_HASH}"', From 7cd8b39593e3a93e2a0be3e3ae42efe23059afdd Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 18:09:04 +0800 Subject: [PATCH 6/8] =?UTF-8?q?Revert=20"=E4=BF=AE=E5=A4=8D=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E9=83=A8=E7=BD=B2=E5=8F=AF=E9=80=89=E6=8F=90=E4=BA=A4?= =?UTF-8?q?=E5=8F=82=E6=95=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d2366bc40a417a0b762303e03db883b296fb2d3d. --- .../shared-memory/decision-log.md | 6 ----- ...Jenkins容器预览部署控制面技术方案-2026-08-15.md | 2 -- jenkins/Jenkinsfile.preview-deployer | 10 +++----- scripts/check-preview-deployer.mjs | 25 ------------------- 4 files changed, 3 insertions(+), 40 deletions(-) diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 1394bf4f0..bc671a867 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -14161,9 +14161,3 @@ - 实例与端口:分支规范化后形成稳定 `deploymentId`,同一分支换 commit 复用实例和 Web 端口;不同分支使用独立 Compose project。Web 端口在全局文件锁内从 `8400..8499` 分配,状态表与宿主监听同时空闲才可占用,卸载后释放。SpacetimeDB 与 OTLP 不映射宿主端口,Jenkins 通过受控 Compose 网络发布模块;页面只展示 Web 内网地址。 - 来源与卸载:部署只接受 `SOURCE_BRANCH` 和可选 `COMMIT_HASH`,Jenkins 必须证明 commit 属于目标分支。卸载只接受受控状态中存在的 `deploymentId`,客户端不能传 Jenkins URL、Job、Compose project、容器名或端口。状态通过固定 `preview-result.json` artifact 返回,不解析或向浏览器暴露完整 console。 - 关联文档:`docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 - -## 2026-08-15 Jenkins 预览部署可选 commit 的跨 shell 参数合同 - -- 决策:`COMMIT_HASH` 在 Preview Deployer 全链路保持可选。控制服务正常请求显式传空字符串;Jenkins 流水线同时兼容首次参数未物化和旧构建重跑,在每个独立 `sh` 中使用 `${COMMIT_HASH:-}` 归一化,禁止依赖前一 shell 的局部变量。 -- `DEPLOY` 缺少 `SOURCE_BRANCH` 必须失败关闭;分支明确而 commit 为空时才表示选择该目标分支的远端最新提交,仍统一经过 `scripts/jenkins-checkout-source.sh`,不得改用控制器 `master`、忽略 commit 归属或关闭严格 shell。 -- 验证方式:`npm run check:preview-deployer` 必须拒绝 checkout 阶段的裸 `${COMMIT_HASH}`;现场重跑不带 commit 的 DEPLOY 后,流水线应通过 `Checkout Requested Source` 并在结果中记录目标分支解析出的 `resolvedCommit`。 diff --git a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md index 2e4c04978..23436d15c 100644 --- a/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md +++ b/docs/technical/【开发运维】Jenkins容器预览部署控制面技术方案-2026-08-15.md @@ -56,8 +56,6 @@ SpacetimeDB 与 OTLP 不映射宿主端口;Jenkins 通过受控 Compose 网络 分支名只允许数字、字母、点、下划线、短横线和斜杠,不允许首尾斜杠或连续点号。Jenkins 必须复用 `scripts/jenkins-checkout-source.sh`,验证 commit 真实属于目标远端分支;SPA 和代理服务的输入校验不能代替流水线校验。 -`COMMIT_HASH` 是真正可选的参数:控制服务应显式传空字符串,但 Jenkins 也必须兼容首次参数尚未物化、旧构建重跑等没有该环境变量的调用。每个独立 `sh` 步骤都要在 `set -u` 下用空值默认完成归一化,不能依赖前一个 `sh` 中的局部赋值。`DEPLOY` 缺少 `SOURCE_BRANCH` 必须在动态 checkout 前失败关闭;分支明确而 commit 为空时,仍由 `scripts/jenkins-checkout-source.sh` 解析为该目标分支的远端最新提交,不得回退到控制器 `master` 或跳过分支归属校验。 - 每次运行归档 `preview-result.json`,至少包含: ```json diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 7ab5b038a..3f4e5961e 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -52,10 +52,6 @@ pipeline { COMMIT_HASH="${COMMIT_HASH:-}" DEPLOYMENT_ID="${DEPLOYMENT_ID:-}" case "${ACTION}" in DEPLOY|STATUS|UNINSTALL) ;; *) exit 1 ;; esac - if [ "${ACTION}" = 'DEPLOY' ] && [ -z "${SOURCE_BRANCH}" ]; then - echo 'DEPLOY 必须提供 SOURCE_BRANCH。' >&2 - exit 1 - fi if [ -n "${SOURCE_BRANCH}" ]; then git check-ref-format --branch "${SOURCE_BRANCH}" >/dev/null fi @@ -97,9 +93,9 @@ pipeline { withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) { sh ''' set -euo pipefail - SOURCE_BRANCH="${SOURCE_BRANCH:-}" \ - COMMIT_HASH="${COMMIT_HASH:-}" \ - GIT_REMOTE_URL="${GIT_REMOTE_URL:-}" \ + SOURCE_BRANCH="${SOURCE_BRANCH}" \ + COMMIT_HASH="${COMMIT_HASH}" \ + GIT_REMOTE_URL="${GIT_REMOTE_URL}" \ SOURCE_COMMIT_FILE=".jenkins-source-commit" \ GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \ GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index e3ce90a2f..32d1b0497 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -55,31 +55,6 @@ assertIncludes( "string(name: 'DEPLOYMENT_ID'", 'Jenkins 必须暴露 DEPLOYMENT_ID。', ); -assertIncludes( - jenkinsfile, - 'SOURCE_BRANCH="${SOURCE_BRANCH:-}"', - 'Jenkins 目标源码 checkout 必须兼容旧构建缺少 SOURCE_BRANCH 环境变量。', -); -assertIncludes( - jenkinsfile, - 'COMMIT_HASH="${COMMIT_HASH:-}"', - 'Jenkins 目标源码 checkout 必须兼容可选 COMMIT_HASH 未物化。', -); -assertIncludes( - jenkinsfile, - 'GIT_REMOTE_URL="${GIT_REMOTE_URL:-}"', - 'Jenkins 目标源码 checkout 必须在严格 shell 下安全读取固定 Git 地址。', -); -assertExcludes( - jenkinsfile, - 'COMMIT_HASH="${COMMIT_HASH}"', - 'Jenkins 不得在 set -u 下严格展开可选 COMMIT_HASH。', -); -assertIncludes( - jenkinsfile, - `if [ "${'${ACTION}'}" = 'DEPLOY' ] && [ -z "${'${SOURCE_BRANCH}'}" ]; then`, - 'Jenkins DEPLOY 缺少 SOURCE_BRANCH 时必须失败关闭。', -); assertIncludes( jenkinsfile, "archiveArtifacts artifacts: 'preview-result.json'", From 1a61c0bea7f3b0561305305f5f39d8701bd08cfd Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 18:12:39 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8DJenkins=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E5=BD=92=E4=B8=80=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 Jenkins 返回的外部构建路径重绑定到受信本机 origin 增加外部 Jenkins URL 的状态与产物轮询回归测试 --- .../preview-deployer-server/src/jenkins.rs | 22 ++++++++++++++++--- .../preview-deployer-server/src/tests.rs | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/server-rs/crates/preview-deployer-server/src/jenkins.rs b/server-rs/crates/preview-deployer-server/src/jenkins.rs index fb4f45437..840c4a0f0 100644 --- a/server-rs/crates/preview-deployer-server/src/jenkins.rs +++ b/server-rs/crates/preview-deployer-server/src/jenkins.rs @@ -274,11 +274,27 @@ impl JenkinsClient { } fn resolve_trusted_url(&self, value: &str) -> Result { - let url = Url::parse(value) + let returned = Url::parse(value) .or_else(|_| self.job_url.join(value)) .map_err(|_| "Jenkins 返回了无效 URL".to_string())?; - self.ensure_same_origin(&url)?; - Ok(url) + if !returned.username().is_empty() + || returned.password().is_some() + || !returned.path().starts_with(self.root_url.path()) + { + return Err("Jenkins 返回了非受信源 URL".to_string()); + } + let relative_path = returned + .path() + .strip_prefix(self.root_url.path()) + .ok_or_else(|| "Jenkins 返回了非受信路径".to_string())?; + let mut trusted = self + .root_url + .join(relative_path) + .map_err(|_| "Jenkins 返回了无效 URL".to_string())?; + trusted.set_query(returned.query()); + trusted.set_fragment(None); + self.ensure_same_origin(&trusted)?; + Ok(trusted) } fn ensure_same_origin(&self, url: &Url) -> Result<(), String> { diff --git a/server-rs/crates/preview-deployer-server/src/tests.rs b/server-rs/crates/preview-deployer-server/src/tests.rs index cbb117687..d78e065e2 100644 --- a/server-rs/crates/preview-deployer-server/src/tests.rs +++ b/server-rs/crates/preview-deployer-server/src/tests.rs @@ -95,7 +95,7 @@ async fn mock_trigger( async fn mock_queue(axum::extract::Path(id): axum::extract::Path) -> Json { Json( - json!({"cancelled": false, "executable": {"url": format!("/jenkins/job/shared/job/Genarrative-Preview-Deployer/{id}/")}}), + json!({"cancelled": false, "executable": {"url": format!("http://192.168.35.82:8080/jenkins/job/shared/job/Genarrative-Preview-Deployer/{id}/")}}), ) } From a312ee20b5cbdaa312861dab763f82fab45ca718 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 15 Aug 2026 18:17:21 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E9=A2=84=E8=A7=88?= =?UTF-8?q?=E6=B5=81=E6=B0=B4=E7=BA=BF=E7=A9=BA=E6=8F=90=E4=BA=A4=E5=8F=82?= =?UTF-8?q?=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 允许 Jenkins 检出阶段安全读取未导出的可选参数 显式使用 bash 执行源码检出脚本 增加空提交参数与脚本执行方式防回归检查 --- jenkins/Jenkinsfile.preview-deployer | 8 ++++---- scripts/check-preview-deployer.mjs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer index 3f4e5961e..3d187edc4 100644 --- a/jenkins/Jenkinsfile.preview-deployer +++ b/jenkins/Jenkinsfile.preview-deployer @@ -93,13 +93,13 @@ pipeline { withCredentials([sshUserPrivateKey(credentialsId: env.GIT_REMOTE_CREDENTIAL_ID, keyFileVariable: 'GENARRATIVE_GIT_SSH_KEY')]) { sh ''' set -euo pipefail - SOURCE_BRANCH="${SOURCE_BRANCH}" \ - COMMIT_HASH="${COMMIT_HASH}" \ - GIT_REMOTE_URL="${GIT_REMOTE_URL}" \ + SOURCE_BRANCH="${SOURCE_BRANCH:-}" \ + COMMIT_HASH="${COMMIT_HASH:-}" \ + GIT_REMOTE_URL="${GIT_REMOTE_URL:-}" \ SOURCE_COMMIT_FILE=".jenkins-source-commit" \ GENARRATIVE_JENKINS_REUSE_EXISTING_CHECKOUT="true" \ GIT_SSH_COMMAND="ssh -i ${GENARRATIVE_GIT_SSH_KEY} -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \ - "${WORKSPACE}/scripts/jenkins-checkout-source.sh" + bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh" ''' } } diff --git a/scripts/check-preview-deployer.mjs b/scripts/check-preview-deployer.mjs index 32d1b0497..472f472fa 100644 --- a/scripts/check-preview-deployer.mjs +++ b/scripts/check-preview-deployer.mjs @@ -70,6 +70,21 @@ assertIncludes( 'disableConcurrentBuilds()', 'Jenkins Job 必须禁止并发构建。', ); +assertIncludes( + jenkinsfile, + 'COMMIT_HASH="${COMMIT_HASH:-}"', + 'Jenkins Checkout 阶段必须允许 COMMIT_HASH 未导出或为空。', +); +assertIncludes( + jenkinsfile, + 'bash "${WORKSPACE}/scripts/jenkins-checkout-source.sh"', + 'Jenkins Checkout 阶段必须显式使用 bash 执行源码检出脚本。', +); +assertExcludes( + jenkinsfile, + 'COMMIT_HASH="${COMMIT_HASH}"', + 'Jenkins Checkout 阶段不得在 set -u 下直接展开未导出的 COMMIT_HASH。', +); assertIncludes(deployer, 'flock -x 9', '部署脚本必须使用跨进程独占锁。'); assertIncludes( deployer,