对于不断演进中的产品,持续交付(CD)使其开发到产品交付的过程更加简单。持续集成(CI)位于持续交付过程的开始阶段,它扮演了这个过程中的重要角色,由它定义软件开发过程。
\在书上和网上可以查到很多持续集成工具的资料,但处于持续集成过程中核心的构建作业却没有太多资料。
\典型的持续集成过程如下:开发人员在他们自己的机器上手工构建和测试源代码。然后他们会把修改提交到一个源码控制管理系统。随后构建工具将运行作业编译和测试这些代码。然后把构建的工件上传到一个中心资源库,用于接下来的开发和测试。
\因此,作业在持续集成工具中的角色是管理源代码不断的修改、运行测试并通过发布通道管理工件传输的物流。
\构建工作的作业任务少则数个,多则上千,所有作业都执行不同的功能。幸运的是,我们可以用一种更加有效的方式去管理这些作业。
\自动化创建构建作业
\那么为什么我们要装置一套自动化创建其他作业的设施?
\构建工具在用户手册中描述了如何创建构建作业,但却很少具体解释如何管理这些作业,尽管这些工具完全可以做到这些事情。
\通常开发人员要了解他们的应用是如何构建的,然后为创建和配置构建作业取得独占性控制权限。然而,这个过程存在一些缺陷:
\- 软件架构约束:由于架构层面的因素,应用很可能有各种不同的构建方式。开发人员可能需要说明他与另一个开发人员在构建应用时的不同,所以一个作业和另一个作业的构建配置总有些细微的差异。因此,如果所有的配置都不同,那么大量的作业就会变得极难维护。\
- 人为因素:手工创建作业引入了出错的风险,尤其是采用先复制再修改的方式去创建一个新作业(即先复制一个已有的作业,然后再修改这个副本,从而得到一个新作业)。\
- 作业时间轴控制:通常情况下,不会保存每次构建的作业配置,也就是说,如果一个作业的配置被修改了,那么就有可能破坏之前的构建。\
所以综合考虑以上几点,如果创建作业时没有一个一致的方法,开发人员就要用自己的设备执行构建了,那么迎接我们的最终结果将是极难维护的持续集成构建系统和令人头痛的管理。这违背了建立持续集成系统的原则:精益和易维护性。
\实际上大多数构建工具都具有这样的设施,可以通过API脚本自动化创建、维护和备份构建作业。然而,尽管构建工具在用户手册里提到过这些功能,但却经常被忽视而未被采用。
\自动化创建构建作业的优点
\为简化持续集成构建过程,可以使用一个主构建作业去自动化创建多个作业,这种做法有以下几个优点。
\- 通过作业模版或脚本创建所有构建作业的配置。它们也可以放在配置控制之下。使用模版或运行脚本可以非常简单地创建一个新作业。这可以显著地缩减作业创建时间(从若干分钟缩短到几秒种)。今后,也可以更加简单地修改作业配置;修改构建作业模版的配置就可以保证所有在之后新创建的作业都会继承这些修改。\
- 所有已有构建作业的配置都保持一致,相比杂乱不一致的系统,这就可以更加简单地通过工具或脚本全局更新所有的配置了。\
- 开发人员在构建作业时就不再需要构建工具的详细知识了。\
- 自动化创建和拆卸构建作业的能力是敏捷持续集成构建生命周期的一部分。\
让我们一起探讨一下下列的几个要点:
\要点1:作业配置的配置控制
\让持续集成系统的每个组件都处于配置控制之下是一个好的实践。这些组件不仅仅包括源代码,还包括基础的构建系统和构建作业。
\大多数构建工具把作业配置以文件形式保存在主系统上。这些文件设定为普通的XML格式,可以直接通过某种REST API前端接口正常地存取这些文件。某些构建工具具备一些内置的特性,可以利用这些特性把作业配置以文件形式保存到任意位置。
\因为可以把作业配置保存为文件,就能够把作业配置以文件形式保存到配置管理(CM)系统中了。不管是直接修改文件然后再上传到构建工具,还是修改构建工具的作业配置,保存这个文件后再把它手工上传到配置管理系统,用这两种方式针对作业配置所做的任何变更都会被记录下来。实际实施哪种方法取决于从构建工具中直接访问作业配置文件的简易程度。
\在配置管理系统中保存作业配置还有一个重要的理由,如果发生灾难性故障,丢失了所有作业配置,构建系统可以比较迅速地恢复,把所有作业、构建日志和构建历史恢复回已知的最近的一次良好状态。
\要点2:作业维护性
\不言而喻,维护上千个不同的配置将是一件令人头疼的事,正因为如此,才使得作业配置的标准化格外重要。如果必需要做一次变更,同时它又会影响到大量相似的作业,那么更加简单的做法是编写脚本或创建特定的任务去修改这些作业。
\对于持续集成来说,在构建系统中有上千个作业无论如何也不是最好的策略。我们将在下面的要点4中对此做更加深入的讨论。
\要点3:开发人员控制
\尽管开发人员被鼓励独立自主地工作,但他们也要使用一个集中的构建系统构建应用程序,他们需要依照构建过程按照指导方针工作。
\开发人员希望他们的代码变更能得到快速反馈,所以不希望在构建工具和作业配置上浪费过多的时间。假如通过一个按钮为开发人员提供“自助服务”式的解决方案,可以自动化地构建作业,就会帮助他们更快地完成开发任务。
\要点4:精益持续集成系统
\尽管软件开发团队在产品研发中采用了敏捷方法,但在他们却不一定在工作中以相同的方式使用构建工具,也就是说已经适当配置过的构建工具不应该包含太多长期的构建作业。理想情况下,应该把作业看作构建持续集成生命周期的一部分,按需来创建。有一种常见的方法是通过任意历史作业配置重新创建构建作业,并保留本次构建痕迹,即日志、工件、测试报告等,然后只在构建工具中保留少量作业。
\当然,这一解决方案(即按需创建-拆卸作业)或许是一个无法达成的目标,但该要点在此主要强调,应该把构建工具中的作业数保持在一个可管理的级别。
\日常开发
\构建管理在治理工具和过程时必须要有助于开发人员的日常活动,能够辅助开发人员在日常完成与构建和持续集成相关的活动。工具提供了授权功能,可以指定哪些开发团队可以创建构建作业,虽然工具提供了灵活的作业配置方式,但仍然要通过软件开发最佳实践的治理。
\构建管理员在治理工具和过程时,应充分考虑开发人员的实际工作,让他们在日常能够更有效地完成构建和持续集成工作。在软件开发最佳实践治理的范畴内尽可能地为负责创建构建作业的团队提供更多的灵活性。
\持续集成作业套件
\思考我们考虑一下开发人员在日常开发过程中执行的典型任务:
\- 构建一个基线\
- 发布基线\
- 构建一个发布分支(经常与发布基线结合在一起)\
- 创建一个开发分支,构建这个分支\
- 运行集成测试\
先抛开最佳开发实践不说(比如尽可能多地在基线上开发、今后几乎没有开发分支等),软件开发团队通常在构建工具中运行构建作业套件完成上文所述的任务。
\现在,作业的自动化创建为软件开发过程带来了巨大的价值;假设一名开发人员为了管理日常运维,打算为所有开发任务都创建四到五个作业,那么用手工方式创建这些作业将耗费大量的时间。相比之下,自动化创建这些作业只需要花费一点点时间去推一下按钮。
\自助服务作业创建解决方案的实现时机
\在下列情况下适合实现创建构建作业的自助服务解决方案:
\- “新建的”项目的初始化(之前从未构建过)。\
- 项目已经存在,但从未给它们建立过任何构建作业。也就是说以前都是从开发人员个人机器上进行的手工构建。\
还有一些情况不适合实现自助服务作业创建解决方案:
\- 如果一个产品只有几个作业,即大型统一性应用,从商业价值来看,就没有必要花费大量的时间,只为那几个少数项目努力地设计、测试和推动自动化创建作业过程。然而,随着业务的扩展,因为要建立数据流管理额外的负载,产品架构将更为复杂,今后构建工具中的作业数会必然有所上升。\
- 已有作业的项目:当在构建工具中处理已有作业时可以考虑以下两个场景。\
- 删除已有作业并用作业组件创建工具重新创建它们,这么做会丢失所有历史构建信息和日志。\
- 保留已有作业,尝试修改它们的配置以符合作业配置的要求,再由作业套件创建工具创建新的作业。\
无论如何,维护遗留的构建作业都难免要花费一些日常管理成本。
\在构建工具中装置自助服务作业套件创建设施
\我们已经讨论过自动化创建构建作业的优点,那么现在让我们一起讨论一下如何在构建工具中实现这样的特性。任何语言(即Java、.NET等)构建应用的原理应该是相同的。
\介绍作业创建构建表单
\构建表单是一种把信息从构建工具直接传到另一个特性或后端脚本的方法,由这些特性和脚本执行作业创建的任务。
\理想情况下,应该尽早提供尽可能多的项目信息,比如像项目名称、源代码的位置和特定构建开关(通常是必需的)等。也就不必为了新建构建作业再来添加额外的配置,从而减少了后期的额外付出。如此,在推动“构建”按钮片刻之后就会创建出作业。
\构建工具实现:
\我们现在要讨论如何在一个构建工具中实现这样的作业创建机制。我曾经使用Hudson 和Anthill Pro 3这两款工具建立过作业套件创建机制,具备一定的个人经验。
\Hudson / Jenkins
\为了清晰起见,我们只讨论Hudson,但讨论的信息对Jenkins同样有用。
\有些管理者希望尽可能地减少他们的开发资源预算,并且不希望受到高价许可的束缚,于是他们非常广泛地采用了Hudson持续集成工具。Hudson把它的构建作业配置定为XML格式。通过顶级页面的作业url可以访问标准的作业配置XML文件,比如:http://hostname/job-name/config.xml
\在配置文件中大多数断落是自解释的。只有在Hudson图形用户界面中使用配置文件时,才能使用插件在文件中增加段落。
\通过用特定的项目信息替换文件中占位符的方式,可以把这些配置文件“模版化”。各个模版可以用来实现不同的任务,比如从不同的软件配置管理系统(例如Subversion或GIT)中检出源代码。
\所以现在我们需要建立创建作业构建表单与脚本的关联。下面的图表展示了工作流程:
\Hudson创建作业的工作流程
\构建表单把信息传给脚本,脚本把模版中的占位符替换为相关信息。然后通过API把配置文件上传到Hudson工具。在浏览器中当前的url后添加上“api” 后可以查阅到关于Hudson API的更多细节,例如http://hostname/job-name/api。
\下面展示了用Hudson API创建作业上传命令的实例:
\\curl –H Content-Type:application/xml –s –data @config.xml \${HUDSONURL}/createItem?name=hudsonjobname\Hudson创建作业表单
\可以为作业手工添加一个新视图。
\Hudson作业视图
\需要注意以下几点:
\- 如果Hudson已配置安全方面的内容,要确保“匿名”用户拥有“创建作业”的权限,否则上传无法通过认证。\
- 如果Hudson选项卡用于查看特定的作业分组,除了通用的“所有”选项卡(或者任何一个使用正则表达式的选项卡)之外,任何最新创建的作业都不可能自动地放在那些选项卡下面。需要升级Hudson主配置文件并手工重启Hudson界面。创建后的作业可以手工添加到相关的选项卡上(如果你非常希望这么做的话)。\
脚本可以用任意语言来编写,比如ANT、Perl、Groovy等等。所以,为了实现前文描述的那些步骤而创作执行脚本其实是相对简单的工作。
\典型的ANT脚本如下所示:
\\\u0026lt;project name=\"createjob\" default=\"createHudsonjobsConfigs\"\u0026gt; \ \\u0026lt;!—为在hudson中创建新作业,更新hudson作业config.xml模版的Ant脚本--\u0026gt; \ \\u0026lt;!--从Hudson构建表单中获取外部属性--\u0026gt; \ \u0026lt;property name=\"hudsonjobname\" value=\"${HUDSON.JOB.NAME}\" /\u0026gt; \ \u0026lt;property name=\"scmpath\" value=\"${SCM.PATH}\" /\u0026gt; \ \u0026lt;property name=\"mvngoals\" value=\"${MVN.GOALS}\" /\u0026gt; \ \\u0026lt;!-- ...以同样的做法从Hudson表单中获取剩余属性--\u0026gt; \... \... \ \u0026lt;property name=\"hudson.createItemUrl\" \value=\"http://hudson.server.location/createItem?name=\" /\u0026gt; \ \\u0026lt;!—包括ant作业扩展--\u0026gt; \ \u0026lt;taskdef resource=\"net/sf/antcontrib/antlib.xml\"/\u0026gt; \ \\u0026lt;target name = \"createHudsonjobsConfigs\" description=\"creates new config.xml file from \ input parameters\"\u0026gt; \ \\u0026lt;mkdir dir=\"${hudsonjobname}\"/\u0026gt; \ \\u0026lt;!-- 依次循环每一个作业模版文件,替换Hudson中作业模版里的属性占位符--\u0026gt; \ \u0026lt;for list=\"ci-build-trunk,RC-build-branch\" param=\"jobName\"\u0026gt; \ \u0026lt;sequential\u0026gt; \ \u0026lt;delete file=\"${hudsonjobname}/${configFile}\" failοnerrοr=\"false\"\u0026gt;\u0026lt;/delete\u0026gt; \ \u0026lt;copy file=\"../job-templates/@{jobName}-config.xml\" tofile=\"${hudsonjobname}\/${configFile}\"/\u0026gt; \ \u0026lt;replace file=\"${hudsonjobname}/${configFile}\" token=\"$HUDSON.JOB.NAME\"\value=\"${hudsonjobname}\"/\u0026gt; \ \u0026lt;replace file=\"${hudsonjobname}/${configFile}\" token=\"$SCM.PATH\"\ value=\"${scmpath}\"/\u0026gt; \ \u0026lt;!-- ...do same for rest of tokens in the job template --\u0026gt; \ ... \ ... \ \u0026lt;antcall target=\"configXMLUpload\"\u0026gt; \ \u0026lt;param name=\"job\" value=\"@{jobName}\"\u0026gt;\u0026lt;/param\u0026gt; \ \u0026lt;/antcall\u0026gt; \ \u0026lt;/sequential\u0026gt; \ \u0026lt;/for\u0026gt; \\u0026lt;/target\u0026gt; \ \\u0026lt;!—构造作业配置上传命令--\u0026gt; \\u0026lt;target name=\"configXMLUpload\"\u0026gt; \ \u0026lt;echo\u0026gt;curl -H Content-Type:application/xml -s --data @config.xml\ ${hudson.createItemUrl}${hudsonjobname}-${job}\u0026lt;/echo\u0026gt; \ \u0026lt;exec executable=\"curl\" dir=\"${hudsonjobname}\"\u0026gt; \ \u0026lt;arg value=\"-H\" /\u0026gt; \ \u0026lt;arg value=\"Content-Type:application/xml\"/\u0026gt; \ \u0026lt;arg value=\"-s\" /\u0026gt; \ \u0026lt;arg value=\"--data\" /\u0026gt; \ \u0026lt;arg value=\"@config.xml\" /\u0026gt; \ \u0026lt;arg value=\"${hudson.createItemUrl}${hudsonjobname}-${job}\"/\u0026gt; \ \u0026lt;/exec\u0026gt; \\u0026lt;/target\u0026gt; \\u0026lt;/project\u0026gt; \\典型的含有占位符的Hudson作业模版如下所示:
\\\u0026lt;?xml version='1.0' encoding='UTF-8'?\u0026gt; \ \u0026lt;project\u0026gt; \ \u0026lt;actions/\u0026gt; \ \u0026lt;description\u0026gt;Builds $POM.ARTIFACTID\u0026lt;/description\u0026gt; \ ... \ ... \ \u0026lt;scm class=\"hudson.scm.SubversionSCM\"\u0026gt; \ \u0026lt;locations\u0026gt; \ \u0026lt;hudson.scm.SubversionSCM_-ModuleLocation\u0026gt; \ \u0026lt;remote\u0026gt;$SCM.PATH/trunk\u0026lt;/remote\u0026gt; \ \u0026lt;local\u0026gt;.\u0026lt;/local\u0026gt; \ \u0026lt;depthOption\u0026gt;infinity\u0026lt;/depthOption\u0026gt; \ \u0026lt;ignoreExternalsOption\u0026gt;false\u0026lt;/ignoreExternalsOption\u0026gt; \ \u0026lt;/hudson.scm.SubversionSCM_-ModuleLocation\u0026gt; \ \u0026lt;/locations\u0026gt; \ \u0026lt;/scm\u0026gt; \ \u0026lt;assignedNode\u0026gt;$BUILD.FARM\u0026lt;/assignedNode\u0026gt; \ ... \ ... \ \u0026lt;builders\u0026gt; \ \u0026lt;hudson.tasks.Maven\u0026gt; \ \u0026lt;targets\u0026gt;$MVN.GOALS\u0026lt;/targets\u0026gt; \ \u0026lt;mavenName\u0026gt;$MVN.VERSION\u0026lt;/mavenName\u0026gt; \ \u0026lt;usePrivateRepository\u0026gt;false\u0026lt;/usePrivateRepository\u0026gt; \ \u0026lt;/hudson.tasks.Maven\u0026gt; \ \u0026lt;/builders\u0026gt; \ ... \ ... \ \u0026lt;/project\u0026gt;\\
ci-build-trunk-config.xml Hudson 作业模版
\Anthill Pro
\Anthill Pro来自于Urban Code有限公司,是一款经许可的构建和持续集成工具。通过它的图形用户界面和大量API库用户可以基本完成构建作业的定制控制。
\uBuild是来自UrbanCode的最新形式,本质上它是Anthill的缩减版,上一代产品最初被设计成管理整条持续发布管道,相比而言它更加主要倾向于面向产品的持续集成构建。uBuild未提供API(Anthill有),但它可以创建一个相同作用的插件。
\在Anthill中把构建作业称为“工作流程”,在本质上它是单个作业任务按特定顺序(串行或并行)执行的管道。
\Anthill的API脚本语言是Beanshell,这门语言基于标准的Java方法。
\Beanshell脚本存储在Anthill图形用户界面里,可以从作业任务中调用这些脚本。Anthill的最新版本可以定制插件,这种插件可以使用任意编程语言来开发。
\上面提到的工作流程中唯一必须的方法是“constructs”,可以使用API调用这个方法创建Anthill作业任务。由于不得不用数据填充工作流程的每个方面,所以脚本可能会特别地长。像Hudson一样,通过执行主工作流程的构建表单收集构建作业信息,再调用Beanshell脚本创建相关的工作流程。
\Anthill作业视图
\Anthill创建工作流表单
\典型的Java Beanshell脚本如下所示:
\\private static Project createProject(User user) throws Exception { \ //获取buildlife属性值 \ String groupId = getAnthillProperty(\"GROUPID\"); \ String artifactId = getAnthillProperty(\"ARTIFACTID\"); \ String build_farm_env_name = \"linux-build-farm\"; \ \ String branchName = \"branchName\"; \ \ Workflow[] workflows = new Workflow[3]; \ //设置项目\ Project project = new Project(artifactId + \"_\" + branchName); \ \ //判定项目是否已存在,是否已** \ boolean isProjectActive; \ try { \ Project projectTest = \getProjectFactoryInstance(artifactId + \"_\" + branchName); \ isProjectActive = projectTest.isActive(); \ } catch (Exception err) { \ isProjectActive = false; \ } \ \ if (!isProjectActive) { \ project.setFolder(getFolder(groupId, artifactId)); \ setLifeCycleModel(project); \ setEnvironmentGroup(project); \ setQuietConfiguration(project); \ \ String applications = getAnthillProperty(\"APPS\"); \ //创建项目属性 \ createProjectProperty(project, \"artifactId\