diff --git a/jenkins/Jenkinsfile.preview-deployer b/jenkins/Jenkinsfile.preview-deployer
index 6591da10f..3d187edc4 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
@@ -89,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/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..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,
@@ -132,6 +147,11 @@ assertIncludes(
'jenkins/Jenkinsfile.preview-deployer',
'Jenkins Job 必须从受保护 master 读取固定流水线。',
);
+assertIncludes(
+ jobConfig,
+ '',
+ 'Jenkins Job 首次触发前必须已经声明 buildWithParameters 参数。',
+);
assertIncludes(
nginx,
'location ^~ /api/preview-deployer/',
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}/")}}),
)
}