Jenkins + Git:只有在 PR 引入子目录更改时才构建
Jenkins + Git: Only build if PR introduced changes in subdirectory
我们有一个大型的 monorepo,里面有多个项目(A 和 B)。我目前将 Jenkins 设置为一个 Multibranch Pipelines 项目,用于监视 PR 的 monorepo。如果创建了 PR,Jenkins 会同时构建 A 和 B。
现在,我希望 Jenkins 变得更聪明,并且只在 PR 中的任何更改导致 A/
目录发生更改时才构建项目 A。这被证明是非常困难的。
when { changeset "A/" }
似乎只检查 上次提交 是否更改了 A/
中的文件,而不检查 PR 更改了 A/
.
中的文件
所以我使用 https://issues.jenkins-ci.org/browse/JENKINS-54285 让它变得更聪明,并且做了:
when { expression { return sourceChanged("A/") } }
与 sourceChanged
定义为:
def boolean sourceChanged(String module) {
if (env.CHANGE_TARGET == null)
return true;
def MASTER = sh(returnStdout: true, script: "git rev-parse origin/${env.CHANGE_TARGET}").trim()
def HEAD = sh(returnStdout: true, script: "git show -s --no-abbrev-commit --pretty=format:%P%n%H%n HEAD | tr ' ' '\n' | grep -v ${MASTER} | head -n 1").trim()
return sh(returnStatus: true, script: "git diff --exit-code --name-only ${MASTER}...${HEAD} {module}") == 1;
}
但是,无论我尝试什么,我都无法获得 CHANGE_TARGET 的提交哈希。我总是遇到以下错误:
git rev-parse origin/master
fatal: ambiguous argument 'origin/master': unknown revision or path not in the working tree.
为什么 Git 找不到 master
、origin/master
、refs/head/master
等(我都试过了)?有没有更简单的方法来完成我想做的事情?
我正在使用来自 docker hub 的 jenkins/jenkins:lts
以及 BitBucket Branch Source 插件。
这里是相关的 Jenkins 日志序列,如果有帮助的话:
Fetching changes from 2 remote Git repositories
> git config remote.origin.url http://bitbucket.ccm.com:7990/scm/JUP/jt.git # timeout=10
Fetching without tags
Fetching upstream changes from http://bitbucket.ccm.com:7990/scm/JUP/jt.git
> git --version # timeout=10
using GIT_ASKPASS to set credentials
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/pull-requests/9/from:refs/remotes/origin/PR-9
> git config remote.upstream.url http://bitbucket.ccm.com:7990/scm/JUP/jt.git # timeout=10
Fetching without tags
Fetching upstream changes from http://bitbucket.ccm.com:7990/scm/JUP/jt.git
using GIT_ASKPASS to set credentials
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/heads/master:refs/remotes/upstream/master
Merging remotes/upstream/master commit 7ef64efeb0fb19d8931a684f147666ae681b4ddf into PR head commit 47600816c0dca3e5555e417085ab2052453a39b2
Enabling Git LFS pull
> git config core.sparsecheckout # timeout=10
> git checkout -f 47600816c0dca3e5555e417085ab2052453a39b2
> git config --get remote.origin.url # timeout=10
using GIT_ASKPASS to set credentials
> git lfs pull origin
> git merge 7ef64efeb0fb19d8931a684f147666ae681b4ddf # timeout=10
> git rev-parse HEAD^{commit} # timeout=10
Merge succeeded, producing 47600816c0dca3e5555e417085ab2052453a39b2
Checking out Revision 47600816c0dca3e5555e417085ab2052453a39b2 (PR-9)
Enabling Git LFS pull
> git config core.sparsecheckout # timeout=10
> git checkout -f 47600816c0dca3e5555e417085ab2052453a39b2
> git config --get remote.origin.url # timeout=10
using GIT_ASKPASS to set credentials
> git lfs pull origin
Commit message: "l"
[Pipeline] withEnv
[Pipeline] {
[Pipeline] sh
+ docker inspect -f . registry.ccm.com:7991/jt:1.0
.
[Pipeline] withDockerContainer
Jenkins seems to be running inside container fdc7e8eec5ea708e59cebe4682651bc5192478b95de803b5981edd222f39af97
$ docker run -t -d -u 1000:979 -v $PWD:/build_env -v $HOME/.ssh:/home/docker_user/.ssh -w /build_env --add-host civm3:10.33.67.183 -e UNIX_USER=jtbuild -w /var/jenkins_home/workspace/jt_PR-9@2 --volumes-from fdc7e8eec5ea708e59cebe4682651bc5192478b95de803b5981edd222f39af97 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** registry.ccm.com:7991/jt:1.0 cat
$ docker top c7bb23bbc91119c2b1875ab2a9186ae34da1754f2b8ae42f758594227ff77137 -eo pid,comm
[Pipeline] {
[Pipeline] sh
+ git rev-parse origin/master
fatal: ambiguous argument 'origin/master': unknown revision or path not in the working tree.
我只想访问 Jenkinsfile 中的两个相关提交 ID:7ef64efeb0fb19d8931a684f147666ae681b4ddf
和 47600816c0dca3e5555e417085ab2052453a39b2
!
好的,我终于解决了。
看起来(如果我的术语不正确,请纠正我)Jenkins 做了所谓的 bare 克隆,这意味着你将无法访问任何引用,除非你特别指定把他们拿来。因此,您将无权访问本地或远程的分支名称。
关键在日志的这两行中:
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/pull-requests/9/from:refs/remotes/origin/PR-9
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/heads/master:refs/remotes/upstream/master
以下是上述两个命令的简短注释版本:
> git fetch the PR ref, store it as 'origin/PR-9'
> git fetch master ref, store it as 'upstream/master'
因此,感兴趣的两个提交存储在 origin/PR-9
和 upstream/master
中。
方便的是,Jenkins 环境变量 BRANCH_NAME
和 CHANGE_TARGET
分别包含 PR-9
和 master
。
因此,Jenkinsfile 应该使用以下内容:
def boolean sourceChanged(String module) {
def target_branch = env.CHANGE_TARGET;
def pr_ref = env.BRANCH_NAME;
if (target_branch == null) {
echo "No target branch defined...";
return true;
}
def TARGET = sh(returnStdout: true, script: "git rev-parse upstream/${target_branch}").trim()
def HEAD = sh(returnStdout: true, script: "git rev-parse origin/${pr_ref}").trim()
echo "Checking for source changes between ${TARGET} (${target_branch}) and ${HEAD} (${pr_ref})...";
return sh(returnStatus: true, script: "git diff --exit-code --name-only ${TARGET}...${HEAD} {module}") == 1;
}
结合,即:
when { expression { return sourceChanged("A/") } }
检查多个目录中的差异将这样完成:
def SOURCE_DIRS = [
"A/",
"X/"
];
...
when { expression { return sourceChanged(SOURCE_DIRS) } }
...
def sourceChanged(ArrayList<String> source_dirs) {
def target_branch = env.CHANGE_TARGET;
def pr_ref = env.BRANCH_NAME;
if (target_branch == null) {
echo "No target branch defined...";
return true;
}
def TARGET = sh(returnStdout: true, script: "git rev-parse upstream/${target_branch}").trim()
def HEAD = sh(returnStdout: true, script: "git rev-parse origin/${pr_ref}").trim()
echo "Checking for source changes between ${TARGET} (${target_branch}) and ${HEAD} (${pr_ref})...";
for (String dir : source_dirs) {
def rc = sh(returnStatus: true, script: "git diff --name-only --exit-code ${TARGET}...${HEAD} ${dir}");
if (rc == 1) {
echo "Changes detected in ${dir}!";
return true;
}
}
echo "No changes detected.";
return false;
}
您还可以使用 Configuration
> Branch Sources
> Behaviours
为您的多分支项目指定额外的引用规范。 Add
> Specify ref specs
,这将使您需要的任何分支名称都可用。
查看此答案以获取屏幕截图:
我们有一个大型的 monorepo,里面有多个项目(A 和 B)。我目前将 Jenkins 设置为一个 Multibranch Pipelines 项目,用于监视 PR 的 monorepo。如果创建了 PR,Jenkins 会同时构建 A 和 B。
现在,我希望 Jenkins 变得更聪明,并且只在 PR 中的任何更改导致 A/
目录发生更改时才构建项目 A。这被证明是非常困难的。
when { changeset "A/" }
似乎只检查 上次提交 是否更改了 A/
中的文件,而不检查 PR 更改了 A/
.
所以我使用 https://issues.jenkins-ci.org/browse/JENKINS-54285 让它变得更聪明,并且做了:
when { expression { return sourceChanged("A/") } }
与 sourceChanged
定义为:
def boolean sourceChanged(String module) {
if (env.CHANGE_TARGET == null)
return true;
def MASTER = sh(returnStdout: true, script: "git rev-parse origin/${env.CHANGE_TARGET}").trim()
def HEAD = sh(returnStdout: true, script: "git show -s --no-abbrev-commit --pretty=format:%P%n%H%n HEAD | tr ' ' '\n' | grep -v ${MASTER} | head -n 1").trim()
return sh(returnStatus: true, script: "git diff --exit-code --name-only ${MASTER}...${HEAD} {module}") == 1;
}
但是,无论我尝试什么,我都无法获得 CHANGE_TARGET 的提交哈希。我总是遇到以下错误:
git rev-parse origin/master
fatal: ambiguous argument 'origin/master': unknown revision or path not in the working tree.
为什么 Git 找不到 master
、origin/master
、refs/head/master
等(我都试过了)?有没有更简单的方法来完成我想做的事情?
我正在使用来自 docker hub 的 jenkins/jenkins:lts
以及 BitBucket Branch Source 插件。
这里是相关的 Jenkins 日志序列,如果有帮助的话:
Fetching changes from 2 remote Git repositories
> git config remote.origin.url http://bitbucket.ccm.com:7990/scm/JUP/jt.git # timeout=10
Fetching without tags
Fetching upstream changes from http://bitbucket.ccm.com:7990/scm/JUP/jt.git
> git --version # timeout=10
using GIT_ASKPASS to set credentials
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/pull-requests/9/from:refs/remotes/origin/PR-9
> git config remote.upstream.url http://bitbucket.ccm.com:7990/scm/JUP/jt.git # timeout=10
Fetching without tags
Fetching upstream changes from http://bitbucket.ccm.com:7990/scm/JUP/jt.git
using GIT_ASKPASS to set credentials
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/heads/master:refs/remotes/upstream/master
Merging remotes/upstream/master commit 7ef64efeb0fb19d8931a684f147666ae681b4ddf into PR head commit 47600816c0dca3e5555e417085ab2052453a39b2
Enabling Git LFS pull
> git config core.sparsecheckout # timeout=10
> git checkout -f 47600816c0dca3e5555e417085ab2052453a39b2
> git config --get remote.origin.url # timeout=10
using GIT_ASKPASS to set credentials
> git lfs pull origin
> git merge 7ef64efeb0fb19d8931a684f147666ae681b4ddf # timeout=10
> git rev-parse HEAD^{commit} # timeout=10
Merge succeeded, producing 47600816c0dca3e5555e417085ab2052453a39b2
Checking out Revision 47600816c0dca3e5555e417085ab2052453a39b2 (PR-9)
Enabling Git LFS pull
> git config core.sparsecheckout # timeout=10
> git checkout -f 47600816c0dca3e5555e417085ab2052453a39b2
> git config --get remote.origin.url # timeout=10
using GIT_ASKPASS to set credentials
> git lfs pull origin
Commit message: "l"
[Pipeline] withEnv
[Pipeline] {
[Pipeline] sh
+ docker inspect -f . registry.ccm.com:7991/jt:1.0
.
[Pipeline] withDockerContainer
Jenkins seems to be running inside container fdc7e8eec5ea708e59cebe4682651bc5192478b95de803b5981edd222f39af97
$ docker run -t -d -u 1000:979 -v $PWD:/build_env -v $HOME/.ssh:/home/docker_user/.ssh -w /build_env --add-host civm3:10.33.67.183 -e UNIX_USER=jtbuild -w /var/jenkins_home/workspace/jt_PR-9@2 --volumes-from fdc7e8eec5ea708e59cebe4682651bc5192478b95de803b5981edd222f39af97 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** registry.ccm.com:7991/jt:1.0 cat
$ docker top c7bb23bbc91119c2b1875ab2a9186ae34da1754f2b8ae42f758594227ff77137 -eo pid,comm
[Pipeline] {
[Pipeline] sh
+ git rev-parse origin/master
fatal: ambiguous argument 'origin/master': unknown revision or path not in the working tree.
我只想访问 Jenkinsfile 中的两个相关提交 ID:7ef64efeb0fb19d8931a684f147666ae681b4ddf
和 47600816c0dca3e5555e417085ab2052453a39b2
!
好的,我终于解决了。
看起来(如果我的术语不正确,请纠正我)Jenkins 做了所谓的 bare 克隆,这意味着你将无法访问任何引用,除非你特别指定把他们拿来。因此,您将无权访问本地或远程的分支名称。
关键在日志的这两行中:
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/pull-requests/9/from:refs/remotes/origin/PR-9
> git fetch --no-tags --progress -- http://bitbucket.ccm.com:7990/scm/JUP/jt.git +refs/heads/master:refs/remotes/upstream/master
以下是上述两个命令的简短注释版本:
> git fetch the PR ref, store it as 'origin/PR-9'
> git fetch master ref, store it as 'upstream/master'
因此,感兴趣的两个提交存储在 origin/PR-9
和 upstream/master
中。
方便的是,Jenkins 环境变量 BRANCH_NAME
和 CHANGE_TARGET
分别包含 PR-9
和 master
。
因此,Jenkinsfile 应该使用以下内容:
def boolean sourceChanged(String module) {
def target_branch = env.CHANGE_TARGET;
def pr_ref = env.BRANCH_NAME;
if (target_branch == null) {
echo "No target branch defined...";
return true;
}
def TARGET = sh(returnStdout: true, script: "git rev-parse upstream/${target_branch}").trim()
def HEAD = sh(returnStdout: true, script: "git rev-parse origin/${pr_ref}").trim()
echo "Checking for source changes between ${TARGET} (${target_branch}) and ${HEAD} (${pr_ref})...";
return sh(returnStatus: true, script: "git diff --exit-code --name-only ${TARGET}...${HEAD} {module}") == 1;
}
结合,即:
when { expression { return sourceChanged("A/") } }
检查多个目录中的差异将这样完成:
def SOURCE_DIRS = [
"A/",
"X/"
];
...
when { expression { return sourceChanged(SOURCE_DIRS) } }
...
def sourceChanged(ArrayList<String> source_dirs) {
def target_branch = env.CHANGE_TARGET;
def pr_ref = env.BRANCH_NAME;
if (target_branch == null) {
echo "No target branch defined...";
return true;
}
def TARGET = sh(returnStdout: true, script: "git rev-parse upstream/${target_branch}").trim()
def HEAD = sh(returnStdout: true, script: "git rev-parse origin/${pr_ref}").trim()
echo "Checking for source changes between ${TARGET} (${target_branch}) and ${HEAD} (${pr_ref})...";
for (String dir : source_dirs) {
def rc = sh(returnStatus: true, script: "git diff --name-only --exit-code ${TARGET}...${HEAD} ${dir}");
if (rc == 1) {
echo "Changes detected in ${dir}!";
return true;
}
}
echo "No changes detected.";
return false;
}
您还可以使用 Configuration
> Branch Sources
> Behaviours
为您的多分支项目指定额外的引用规范。 Add
> Specify ref specs
,这将使您需要的任何分支名称都可用。
查看此答案以获取屏幕截图: