gitlab-ci-multi-runner 1.0.4 (014aa8c) Using Shell executor... Running on appdevdeiMac.local... Fetching changes... HEAD is now at 826d126 Merge branch 'master_dev' of http://yourcompany.com/yourgroup/framework_android into master_dev From http://yourcompany.com/FFProject/appframework_android 826d126..13ba8f3 master_dev -> origin/master_dev Checking out 13ba8f33 as master_dev... Previous HEAD position was 826d126... Merge branch 'master_dev' of http://yourcompany.com/yourgroup/framework into master_dev HEAD is now at 13ba8f3... [master_dev][bank][c:panweizhou][r:chendingyi]update ci script. $ curl -X POST -F token=2587402741a9ef3a09d0dd64f83b90 -F ref=$CI_BUILD_REF_NAME http://yourcompany.com/api/v3/projects/11/trigger/builds % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0100 298 100 26 100 272 220 2304 --:--:-- --:--:-- --:--:-- 2324 {"id":48,"variables":null} Build succeeded.
前面两种方案走不通,我开始思考:Git 难道就没有关于子模块持续集成的 best practice 吗?直到我看到了 blahgeek 的这篇文章 ,里头提出用 commit id 的改动来触发工程更新,顿时恍然大悟:Git 本身建议通过在主工程记录子模块的 commit id 来控制子模块的版本。除了控制版本,commit id 其实还有另一个好处,那就是持续集成!子模块发生修改后,为了让主工程同步该子模块的更新,你需要不断往上提交上层模块的 commit id ,这就会顺带触发主工程的持续集成。
然而,上一篇文章中提到,为了避免只提 commit id 没提代码的情况发生,我们直接禁止了 commit id 的提交。矛盾来了。
幸运的是我们有折中的办法。如果子模块代码已推送成功,那么此时该模块在父工程中的 commit id 一定可以更新。而这个更新为什么不能让计算机帮忙自动完成?我只需要在子模块的中央仓库中加入 post-receive 钩子,当子模块代码推送完成时,post-receive 钩子里的脚本就会自动被触发,帮助我们到上层提交该子模块的 commit id 。对于嵌套子模块,这个过程会一直递归地做,直到父工程就是主工程为止,而这最终就会触发主工程的持续集成!
方法听起来可行,但实际做起来我依然遇到了不少困难。
首先,服务器上的仓库都是 bare repository ,不能提交代码,也没有相互依赖关系,主工程和所有子模块的仓库都是平级的存放在同个目录下的。这意味着你无法利用 post-receive 钩子原地地修改自身仓库和依赖它的其他仓库。
其次,依赖每个子模块的父工程及分支各不相同。当一个子模块的某个分支有更新时,你需要为父工程中为所有依赖该子模块那个分支的全部分支都提交一遍新的 commit id 。
解决第一个问题的方法就是在服务器也像本地那样 clone 出一份整个工程的 working repository ,这个工程和我们本地开发的仓库没什么区别,交给服务器来自动维护。唯一的难点在于怎么将每个 bare repository 与该 working repository 里的每个子模块相关联。于是,只需要写个工具,遍历一遍所有主工程分支,并生成每个分支所依赖的每个子模块的仓库地址与本地路径信息。内容类似这样:
有了这两个文件,post-receive 钩子也就可以写得通用化:先获取该子模块的仓库名,然后根据这个文件找到在 working repository 下对应的目录,然后用 fmanager 切到依赖该子模块该分支的主工程。更新该子模块的 working tree ,最后 cd 到上级目录提交该子模块的 commit id 。
with FileLock(lock_file_name): print ("Updating commit id") # read global module config global_module_config = {} try: withopen(module_file) as f: global_module_config = json.load(f) except ValueError: print("Global modules.json parsed Error!") exit(1)
# Read in each ref that the user is trying to update for line in fileinput.input(): line = line.strip() (from_commit, to_commit, ref) = line.split(' ')
# Get branch name pos = ref.rfind('/') if pos >= 0: branch = ref[pos+1:] else: branch = ref
if branch == 'master'or branch.lower().endswith('bank') or branch.endswith('_dev'): # if branch == 'WeiZhouBank_dev': # Get repo name output = subprocess.Popen(['git summary | egrep "^ project"'], env={"PATH": env_path}, stdout=subprocess.PIPE, shell=True) oc = output.communicate()[0] repo = oc.split(':')[1].strip()
# Get the corresponding working path module_path_info = {} withopen (module_path_file) as f: module_path_info = yaml.load(f) module_path = "" module_name = "" for module_name, module_info in module_path_info.items(): if module_info.has_key('repo'): module_repo = module_info["repo"] if module_repo.endswith(repo): has_such_module = True module_path = module_info["path"] break # Get all the main projects branch that use this module main_branch_set = set() for branch_name, config_list in global_module_config.items(): if config_list.has_key(module_name): mconfig = config_list[module_name] if (mconfig.has_key('branch')): mbranch = mconfig['branch'] if branch == mbranch: main_branch_set.add(branch_name)
# Do the following stuff for each branch of main project # that use this module of this branch for main_branch in main_branch_set: os.chdir(project_root) output = subprocess.Popen(["./fmanager checkout -f %s" % main_branch], env={"PATH": env_path}, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) res = output.wait() if res == 0: os.chdir(module_path) # update current submodule output = subprocess.Popen(['/home/git/bin/update_root', branch], cwd=module_path, env={"PATH": env_path}) res = output.wait() if res == 0: # Get commit log output = subprocess.Popen(['git log -n 1'], env={"PATH": env_path}, cwd=module_path, stdout=subprocess.PIPE, shell=True) commit_log = output.communicate()[0] commit_log = "Bump Version for submodule %s:\n\n%s" % (module_name, commit_log) # Go to father module os.chdir('..') father_path = os.getcwd() # Get branch of father module output = subprocess.Popen(['git', 'symbolic-ref', '--short', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={"PATH": env_path}) oc = output.communicate() father_branch = oc[0].strip() if (father_branch.strip() != ""): print("Bumping version of branch %s of father project..." % father_branch) output = subprocess.Popen(['/home/git/bin/update_root', father_branch], cwd=father_path, env={"PATH": env_path}) res = output.wait() if res == 0: output = subprocess.Popen(['git diff | wc -l'], cwd=father_path, env={"PATH": env_path}, stdout=subprocess.PIPE, shell=True) diff_num = output.communicate()[0] if diff_num > 1: subprocess.call(['git', 'add', module_name], env={"PATH": env_path}) output.wait() output = subprocess.Popen(['git', 'commit', '-m', commit_log, '--no-verify'], env={"PATH": env_path}) res = output.wait() if res == 0: output = subprocess.Popen(['git', 'push', 'origin', 'HEAD', '--no-verify'], env={"PATH": env_path}) output.wait() if res == 0: print"Successfully bumped version branch %s of father project" % father_branch else: print"Error bumping version for branch %s of father project" % father_branch else: print"Error bumping version for branch %s of father project" % father_branch else: print"Error bumping version for branch %s of father project" % father_branch else: print("Father project is a tag. Stop bumping verison.") else: print"Error updating the remote working tree."
之后只需将钩子安装到每个子模块的 bare repository 里的 custom_hooks 目录下。同样可以利用脚本来一次性完成。
for module in `ls -d *_android.git` do iftest$module != "App_Android.git"# Don't install to root project then module_dir=${module} iftest ! -d ${module_dir}/custom_hooks then mkdir ${module_dir}/custom_hooks fi for hook in `ls submodule_hooks_android` do cp submodule_hooks_android/${hook}${module_dir}/custom_hooks/ sudo chmod -R 755 ${module_dir}/custom_hooks sudo chmod +x ${module_dir}/custom_hooks/${hook} done fi done
钩子装好后,试着为 framework 子模块推送一下代码,终端中会看到 Bump Version for submodule framework 的字眼,表示 framework 的 commit id 已被成功更新到主工程: