CI 持续集成学习总结 //附件:持续集成相关研究总结.doc
持续集成学习总结
(一)持续集成概述
1.定义
大师Martin Fowler对持续集成是这样定义的:
持续集成是一种软件开发实践,即团队开发成员经常集成它们的工作,通常每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽快地发现集成错误。许多团队发现这个过程可以大大减少集成的问题,让团队能够更快的开发内聚的软件。
2.原则
(1)所有的开发人员需要在本地机器上做本地构建,然后再提交的版本控制库中,从而确保他们的变更不会导致持续集成失败。
(2)开发人员每天至少向版本控制库中提交一次代码。
(3)开发人员每天至少需要从版本控制库中更新一次代码到本地机器。
(4)需要有专门的集成服务器来执行集成构建,每天要执行多次构建。
(5)每次构建都要100%通过。
(6)每次构建都可以生成可发布的产品。
(7)修复失败的构建是优先级最高的事情。
3.要素
统一的代码库
自动构建
自动测试
每个人每天都要向代码库主干提交代码
每次代码递交后都会在持续集成服务器上触发一次构建
保证快速构建
模拟生产环境的自动测试
每个人都可以很容易的获取最新可执行的应用程序
每个人都清楚正在发生的状况
自动化的部署
4.周期
一个典型的持续集成周期包括以下几个步骤:
持续集成服务器不断从版本控制服务器上检查代码状态,看代码是否有更新。
如果发现代码有最新的提交,那么就从版本控制服务器下载最新的代码。
等代码完全更新以后,调用自动化编译脚本,进行代码编译。
运行所有的自动化测试。
进行代码分析。
产生可执行的软件,能够提供给测试人员进行测试。
5.价值
持续集成的价值在于:
﹡减少风险
一天中进行多次的集成,并做了相应的测试,这样有利于检查缺陷,了解软件的健康状况,减少假定。
﹡减少重复过程
减少重复的过程可以节省时间、费用和工作量。说起来简单,做起来难。这些浪费时间的重复劳动可能在我们的项目活动的任何一个环节发生,包括代码编译、数据库 集成、测试、审查、部署及反馈。通过自动化的持续集成可以将这些重复的动作都变成自动化的,无需太多人工干预,让人们的时 间更多的投入到动脑筋的、更高价 值的事情上。
﹡任何时间、任何地点生成可部署的软件
持续集成可以让您在任何时间发布可以部署的软件。从外界来看,这是持续集成最明显的好处,我们可以对改进软件品质和减少风险说起来滔滔不绝,但对于客户来 说,可以部署的软件产品是最实际的资产。利用持续集成,您可以经常对源代码进行一些小改动,并将这些改动和其他的代码进行集成。如果出现问题,项目成员马上就会被通知到,问题会第一时间被修复。不采用持续集成的情况下,这些问题有可能到交付前的集成测试的时候才发现,有可能会导致延迟发布产品,而在急于修复这些缺陷的时候又有可能引入新的缺陷,最终可能导致项目失败。
﹡增强项目的可见性
持续集成让我们能够注意到趋势并进行有效的决策。如果没有真实或最新的数据提供支持,项目就会遇到麻烦,每个人都会提出他最好的猜测。通常,项目成员通过手工收集这些信息,增加了负担,也很耗时。
持续集成可以带来两点积极效果:
―有效决策:持续集成系统为项目构建状态和品质指标提供了及时的信息,有些持续集成系统可以报告功能完成度和缺陷率。
―注意到趋势:由于经常集成,我们可以看到一些趋势,如构建成功或失败、总体品质以及其它的项目信息。
﹡建立团队对开发产品的信心
持续集成可以建立开发团队对开发产品的信心,因为他们清楚的知道每一次构建的结果,他们知道他们对软件的改动造成了哪些影响,结果怎么样。
6.目的
持续集成实践的目的不是减少Build失败的次数,而是尽早发现问题,在最短的时间内解决问题,减少风险和浪费。如果想尝试持续集成,首先需要的是持续集成服务器,比如Cruise Control或者VSTS;然后需要把现有的Build自动化,比如写Ant脚本;最后就是在持续集成服务器上进行配置,比如配置版本控制,集成间隔时间,如何部署,如何反馈等。
(二)持续集成流行工具初步分析与建议
持续集成的工具现如今也是百花齐放,各有千秋,接下来我们主要介绍一下目前主流的持续集成工具。
我们将从以下几个方面来进行介绍;
1. 厂商
2. 支持的编程语言
3. 价格
4. SCM支持程度
5. 构建管理
6. 消息通知机制
7. 构建工具支持
8. 项目管理工具集成
9. 测试工具集成
10. 安装及配置
11. IDE集成
Ⅰ.CruiseControl.( http://cruisecontrol.sourceforge.net/)
厂商 |
ThoughtWorks |
开发语言 |
JAVA,也有.net和ruby版本 |
是否开源 |
是 |
价格 |
免费 |
SCM 支持程度 |
ClearCase , VSS, CVS, Subversion, PVCS 等 |
构建管理 |
并行构建,分布式构建,增量构建,人工强制构建, SCM 触发构建等都有支持 |
消息通知机制 |
Email , Run executable,FTP,IRC,Jabber,Lotus Sametime,RSS,SCP , Windows System Tray , Formatted Logging , Yahoo Messenger , X10 |
构建工具支持 |
Shell 脚本与命令行, Ant, OpenMake Meister, Maven, Maven2, NAnt |
项目管理工具集成 |
项目管理工具 CruiseControl 支持了 Rally 和 VersionOne |
测试工具集成 |
Agitar , JUnit result rendering |
安装与配置 |
有 windows 安装程序, Self contained distribution (except SCM clients) , N 无需修改构建脚本,支持多个项目,使用 XML 配置文件 |
IDE 集成 |
Eclipse Plugin , IntelliJ Plugin |
Ⅱ.LuntBuild ,它的商业版本是 QuickBuild. (http://www.quickbuild.com.cn/index.php)
厂商 |
PMEase |
支持的编程语言 |
Java |
是否开源 |
是 |
价格 |
免费 |
主流 SCM 支持程度 |
Clear Case , VSS, CVS, Subversion |
构建管理 |
并行构建 , 增量构建,人工强制构建, SCM 触发构建 |
消息通知机制 |
Email , Run executable , Jabber , Lotus Sametime , RSS , Windows System Tray , Formatted Logging , MSN Messenger
|
构建工具支持 |
Shell 脚本与命令行, Ant, OpenMake Meister, Maven, Maven2 , MSbuild , NAnt , Rake (Ruby) |
项目管理工具集成 |
无 |
测试工具集成 |
JUnit result rendering , Selenium result rendering , PHPUnit result rendering , MSTest result rendering |
安装与配置 |
有 windows 安装程序, Self contained distribution (except SCM clients) , N 无需修改构建脚本,支持多个项目 |
IDE 集成 |
Eclipse Plug-in |
Ⅲ.Hudson,目前使用最广的持续集成工具.(http://hudson-ci.org/)
厂商 |
Java.net |
支持的编程语言 |
Java |
是否开源 |
是 |
价格 |
免费 |
主流 SCM 支持程度 |
Clear Case , VSS, CVS, Subversion , PVCS 等, SCM 支持最为完善 |
构建管理 |
并行构建,分布式构建,增量构建,人工强制构建, SCM 触发构建等都有支持 |
消息通知机制 |
Email , Run executable , FTP , IRC , Jabber , Lotus Sametime , RSS,SCP,Windows System Tray,Formatted Logging
|
构建工具支持 |
Shell 脚本与命令行, Ant, Groovy, OpenMake Meister, Maven, Maven2 , MSbuild , NAnt , Rake (Ruby) |
项目管理工具集成 |
无 |
测试工具集成 |
CppUnit result rendering , JUnit result rendering , NUnit result rendering , Selenium result rendering , PHPUnit result rendering , MSTest result rendering , SilkCentral , Clover result rendering , PMD result rendering |
安装与配置 |
有 windows 安装程序, Self contained distribution (except SCM clients) , N 无需修改构建脚本,支持多个项目,自动配置构建脚本 |
IDE 集成 |
Eclipse Plug-in , IntelliJ Plugin |
Ⅳ.持续集成流行工具的初步分析与建议
持续集成(continuous integration)作为敏捷编程的基石现在已经被绝大多数的开发团队所广泛采用。而持续集成的工具现如今也是百花齐放,各有千秋,在此主要对比了在Java领域中比较常见的几种CI server(因为公司要求统一整个公司的CI server)。如果想了解更多的工具,可以看这里:
http://confluence.public.thoughtworks.org/display/CC/CI+Feature+Matrix
在此主要针对以下几种CI Server作对比,版本有点多,国内的多选用了一些open source的,而老外那边用得比较多的是商用版本;
CruiseControl (http://cruisecontrol.sourceforge.net/)
Hudson (https://hudson.dev.java.net/)
LuntBuild (http://luntbuild.javaforge.com/)
TeamCity (http://www.jetbrains.com/teamcity/)
AntHill Pro (http://www.anthillpro.com/)
Bamboo (http://www.atlassian.com/software/bamboo/)
QuickBuild (http://www.pmease.com/)
在持续集成领域,OpenSource的CruiseControl和LuntBuild可谓老牌了,尤其是CruiseControl,出自thoughtworks,这可是Martin Fowler的老巢啊。Hudson作为OpenSource里持续集成的后起之秀,现在已经赶超了这两个前辈,目前恐怕是使用最多的一个CI Server了。而后面4个是商用的CI Server,其中TeamCity是来自jetbrains的,jetbrains是开发著名的IDE IntelliJ的公司。Bamboo则是开发著名的Bug Tracking工具Jira和Wiki Confluence的公司atlassian公司出品的。AntHill也属于Continuous Integration界的元老,QuickBuild则是LuntBuild的商业版本,我在下面重点考量的是QuickBuild,因为LuntBuild好像现在更新较慢了,而且QuickBuild现在好像也有了免费的所谓的Community Edition,功能齐全,只是配置数有所限制。在这些商业版本中,TeamCity应该是目前市场占有率最高的。由于公司里比较倾向使用商业版本的服务器,所以我重点比较的是后4种,捎带比较了一下CruiseControl和Hudson。TeamCity和QuickBuild都有各自的免费版本,有兴趣的也可以去看看。
CI Server在本质上就是一个定时调度器。我们配置一系列的项目,然后设定一个定时器,让它干一些活,然后通知大家。所以很多公司都使用所谓Home-made的工具,用cron+Ant/Maven来做持续集成,这个就已经可以达到CI的最简单的功能了。而使用工具,就是我们除了基本的编译和通知功能以外,我们还有很多其它的需求,在公司里,选择CI Server主要考虑以下几点:
·便于公司的统一管理(大约有200+ Projects需要统一管理)
·对于项目本身进行流程管理: Daily Build -> QA Build -> Release Build
·公司AD(Active Directory)的连接以对用户进行权限管理
·Continuous Testing的支持,即对于项目的Test要能产生出详尽的报告以及收集Test的统计数据以作为项目的分析和考量
·Continuous Code Quality Analysis的支持,即能处理项目产生的Coverage报告,Code的static analysis报告,并且能收集这些报告的统计数据以作项目的分析和考量
·与SCM工具的集成,我们公司主要有三种VCS,ClearCase, Subversion和StarTeam
·与其它工具的集成,如bug tracking工具,IDE集成等等。
安装CI
安装是我们开始的第一步,同时也对各个CI server都有了初步的印象。按照各自的手册,很快就装好了,我基本上选择的是Standalone的版本,就是不配置数据库,使用自带的,也不deploy到Tomcat或者其它容器,这点,基本上每个CI Server都非常简单。
AntHill要download还得提交一个request,然后才能下载,安装。
配置项目
在大多数的CI Server中,绝大部分都是以Project或者Project Group来进行管理,只有LuntBuild和QuickBuild比较另类,它们使用了Configuration这个术语,意即一个配置。在配置一个典型的项目的时候,即只处理基本的一个流程:CheckOut, Build, Publish Artifacts,这些工具都完成的非常好,也非常简单。
TeamCity的导航最方便,一目了然。
而LuntBuild和QuickBuild在这方面稍显人性化不足,这两个工具都没有使用wizard的模式。
下面,我接着实验配置50个测试项目,这也就开始考验一个CI Server的管理能力了。
QuickBuild最实用,因为它使用Configuration而不是Project,并且它是这些CI Server中唯一支持树状结构配置的。我可以把Configuration配置成Team A, Team B ...,然后根据实际情况,对每个Team配置任意多个子节点,孙节点(注意,Configuration的数目在QuickBuild的Community Edition里是要限制的,好像是最多16个)。QuickBuild的继承关系使用起来也非常方便,如果要管理一个大型的CI Server,没有这种继承简直是一种折磨。比如说用hudson来配置50个项目,要需要大半天,而用QuickBuild来,只需大约一个小时,我实际配置的Configuration(含有实际step定义的)只有3个,其它的都是继承下来,然后修改了一下参数而已,而如果我们需要批量修改一系列的configurations的时候,则由于有继承关系,通常我们只要去修改一下父节点的设置就可以了。
TeamCity支持Project Group的概念,类似于一种树形,但是还不完备,它只能分成两级关系,即Project Group和Project。另外QuickBuild所拥有的继承的功能,在别的CI里没有看到过,有的只是象TeamCity类似的copy project的功能。而QuickBuild在复制的能力上远远胜过其它的CI Server,它可以整个子树拷贝,这也就意味着,我可以配置一个公司用的template configuration树,然后复制出A部门,B部门,C部门,等等等等。对于不同项目之间的区别则通过变量来控制,赞一个!TeamCity在配置的方便上真得是没话说,非常直观,最酷的是象JUnit,NUnit这样的Tests,连Ant脚本都不需要写了,它直接就可以找出项目里的unit tests,这个在其它的工具里也没有看到过。
至于CruiseControl,Hudson,Bamboo等则是中规中矩,无甚亮点。
另外配置一个项目要配的就是项目持续集成的流程管理,在我们这里,基本上是这样一个流程: Daily Build -> QA Build -> Integration Build -> Release Build。所谓Daily Build,顾名思义,就是每天一次的,由development team管理以保证项目的顺畅执行,然后经过一段时间后,development team要提交到QA那边进行测试,通常是2个星期到一个月左右,随项目大小不等,QA测试结束之后,如果没有重大的问题,则提交作Integration Test,以保证在模拟的实际环境中能正常工作,最后,如果没有什么问题的话则作Release Build以形成发布版本。对于公司里有一些Team使用敏捷编程的,则需要增加所谓的Commit Test Build,也就是developer在作每一个check in的时候自动触发一个build,以保证build不会被这个check in破坏(包括不会破坏unit tests和code quality)。这也是所谓的要作continuous testing和continuous code quality analysis,这些都是通过利用JUnit, NUnit,CheckStyle, PMD,Cobertura,FxCop等工具来实现的。
这个环节里,个人比较喜欢AntHill Pro和QuickBuild,这两个工具都是比较强调流程的,尤其是AntHill Pro更是将其作为卖点。AntHill Pro以工作流的模式来定义这个流程,一个项目可以定义多个的workflow,对应于我们的case,就是定义Daily Build的workflow,定义QA Build的workflow,等等,然后在作promote的时候,通过选择不同的workflow来达到目的。
QuickBuild则是利用已有的configuration的概念,定义不同的Configuration,然后在Configuration的setting里定义一个或多个要promote的configurations。要作promote的时候,则通过点击某个build的promote按钮将其promote到指定的configuration上去,也很方便。使用AntHill的模式,概念上很清晰,因为我们要作的是流程管理嘛,所以workflow会听起来比较容易接受。而QuickBuild则是把它绑定在Configuration上,使用起来比较简单,但是找起来要费点事,至少对于我而言是这样。Hudson也有类似的流程管理,但是它是自动的,而promote在我们这里是需要人来作review的,也就是说要人去参与,判断究竟使用哪个版本来promote,所以在我们这里,不是很合适。
在配置项目这个环节里,个人感觉QuickBuild比较灵活,既可以做到很简单的配置,也可以做到非常复杂的配置,而且配置起来方便性非常好。只是术语与其它的CI Server有些不同,需要熟悉一下。
Build功能:CI Server最重要的就是Build本身的功能,包括SCM的连接,用户的权限管理,Build工具的支持。首先我们来看看SCM的支持。
SCM支持
在这些CI Server中,AntHill Pro和Hudson支持的种类最多,尤其是Hudson,基本上市面上的SCM都有所支持。对于象比较常见的Subversion,CVS,ClearCase,StarTeam,SourceSafe等,各家都已经支持了。
而QuickBuild,则属于在SCM里支持最少的一家,它还不支持git,Team Foundation Server,这个目前已经很流行的两种SCM,QuickBuild在支持SCM的时候,由于使用变量的支持,却是多家CI Server中最灵活的一家,它可以使用变量来配置SCM的URL,而其它的,则是通过定义一个基本的URL,然后针对不同项目来定义各自的SCM repository。而QuickBuild还有一个它自有的QuickBuild Repository,用于在不同的Configuration中传递artifacts,实际用起来也很方便,比如说我们在一个项目里要用到别的项目的artifacts,那么就可以定义一下这个repository。当然,这个功能也可以通过Maven的repository来完成来达到相同的目的。TeamCity也提供了类似的机制,只不过TeamCity的Repository其实就是一个Ivy的扩展。
SCM的数据在这些CI Server中都有体现,从每一个Build的change sets到历史统计。说明现在大家都很重视对于这些数据的收集和分析。其中TeamCity能直接从Web页面上直接调用IDE来打开这些改动的文件是一大亮点,毕竟是做IntelliJ的公司!
用户管理
这个基本上是每个CI Server的必备功能了,基本上都是既可以用内置的数据库管理(Hudson好像没用数据库),又可以连接LDAP服务器。
Build的Dependencies管理 (Dependent Builds)
在实际的项目中,我们常常会出现项目之间的依赖关系,比如说A项目依赖于B项目,B项目依赖于C项目。所以当我们要编译A项目的时候,我们需要先编译C项目,然后编译B项目,最后再来编译A项目,这样做的好处显而易见,就是保证我们总是使用最新开发的code来编译一个版本,如果发生了什么问题,我们也可以很容易的知道究竟是哪个项目break了整个build的流程。这个功能基本上所有的这些CI Server都有提供,而能力各有千秋。
TeamCity在这里属于最弱的一个,它只能通过定义Ivy来达到Artifacts在不同项目中的依赖管理。
而AntHill Pro,Bamboo和QuickBuild则都有提供两种类型的dependency管理,即artifacts和项目本身的依赖管理。
不过TeamCity却有另外的杀手锏,就是导入项目的功能,它支持从IntelliJ的项目,Maven的项目中直接导入创建这种依赖关系。
分布式Build Pool
由于公司的项目繁多,平台繁多,对于一个项目需要分布到不同的平台去编译,测试,这时候就需要建立一个Build Pool了,基本上述各家的CI Server都已经支持了这种分布式的build pool,其实质是利用了grid computing技术来进行管理。也就是一个build server带上一群的build agent,然后把build的任务分布到不同的agent上去执行。
在这里不得不再赞一个QuickBuild了,其实QuickBuild的agent与其它家的倒没什么不同,只不过就是一个computing unit,关键在于QuickBuild里配置一个configuration,它使用了step的概念,这个step在AntHill Pro里也存在,关键在于这个step是可以分布的,也就是说,我配置一个项目的时候,可以定义一系列并行的分布式的step,这样对于管理和收集artifacts非常方便,我们可以定义Test On Windows, Test On Mac, Test On Linux,然后设置一下运行这些step的时候需要什么类型的agent,QuickBuild就可以把这些任务分布到这些平台的agents上去运行了。
而其它家的可能是因为收费的方式,象TeamCity,一个build只能在一个agent上运行,我如果要做到同样的效果,就需要定义出三个项目,然后让这三个项目在不同的agents上运行,最后,还要再定义一个项目,让这个项目去收集它们的artifacts,非常麻烦。
Bamboo和AntHill也类似于TeamCity。
而Hudson在这块的能力很弱,个人感觉不如其它的产品强大,而且使用起来也更复杂一些。
Report功能和统计
上述各家CI SERVER都提供了Report的功能和统计的功能。
Hudson是支持报告类型最多,最全的,因为是OpenSource的,有太多人开发。
Bamboo属于支持报告类型最少的,不过也有很多第三方的plugin(插入式的)供选择。
我们所关心的几个reports都有被各家支持;
QuickBuild的report给我的感觉最华丽,不过好像是参考google analytics来的,从界面上看和analytics简直就是一个翻版。
在使用上,QuickBuild和TeamCity的最方便,直接点报告中的链接就可以作一些过滤。
在统计信息方面,各家对tests的统计都非常完备,这也从一个侧面反应出test driven现在已深入人心。
在支持Test Driven方面,TeamCity是力拔头筹,得益于开发IntelliJ的经验,TeamCity不仅可以自动寻找出项目中的unit tests(你不用在Ant脚本里调用junit task,或者在Maven里调用surefire),而且对于上次运行失败的test cases,它可以在下次build中自动先运行,这样就可以避免一个build运行了很久才发现上次失败的test还没有被更正过来呢。
另外,要提一下,QuickBuild中那个Build的Dashboard我非常喜欢,对于一个项目当前的状况可以一目了然,有多少个tests成功了,多少失败了,多少被fix了,多少还没有fix,总之,信息很丰富,不过就是配置起来有点复杂,需要我去一个报告一个报告去加step,如果能做到TeamCity的程度,简直就是完美了。
对于其它的CI Server则是亮点不多。其实也很强,只不过是对比而言,我觉得TeamCity和QuickBuild更强,更好。
与第三方工具的集成
在与第三方工具的集成中,Hudson遥遥领先,是所有CI Server里Plugin最多的。可以和FaceBook,Google Calendar,Twitter,反正基本上你能想到的,它都有。不过对于我们而言,好多Plugin没有太大的价值。Bamboo在与它自己的几个产品中集成度也非常好,比如说Jira,Wiki,Clover等。这几个我们公司都有用到,在这点上非常理想。
价格
考虑一下价格的因素,记得有人说过,Price is nothing, but price is everything,尤其在这个金融危机的年代里。OpenSource永远是最好的。而在商用的这几个里QuickBuild最便宜,它使用的是Site License,一个Site收$2999,AntHill最贵,随便搞搞就要$10000了,TeamCity的入门也很便宜,$1999带3个agents,它是按agent收费的,Bamboo也很贵,按照它的功能而言,性价比不是很好。
总结
综合各方面因素,基于原笔者的考虑,最后得出结论比较倾向于QuickBuild,虽然这个产品名声不是很大,不过在它的客户中,不乏象Cisco,HP这样级别的公司,应该还是可以值得信赖的。另外就是我们使用下来觉得它还是拥有诸多亮点,对于统一管理来说,可谓是方便至极。另外价格方面考虑也很不错。当然如果你的团队不是很大,那么选择QuickBuild的Community Edition和TeamCity的Professional Edition都是非常值得,这两者都是免费的,而且QuickBuild的Community Edition功能没有任何裁剪,只是限制了一下configuration的数目,非常适合要求比较高而项目不是很多的团队。
(三)持续集成的具体实施经验举例
持续集成有很多很多的好处。可是持续集成要做好的话,本身就有很多的讲究。从持续集成工具的选择到持续集成具体实施,每一点都可能影响到你使用持续集成的效果。持续集成不是持续编译,也不是仅仅用来发发邮件的工具而已。
首先选择一个好的工具很重要,可能会觉得QuickBuild这个工具真得很不错。工具选好了,具体怎么做呢?这个没有什么标准可以遵循,每个项目都是不一样的,我谈谈我们这里的具体过程吧。
首先,我们对编码有一些规范需要遵从,所以我们制定了一系列的FindBugs和PMD的规则用于检查代码。
其次,我们使用Cobertura作为我们的代码覆盖(code coverage)工具。
再次,我们使用JUnit作为我们的unit test工具
基于上述几点,我们编写了我们的Ant脚本,这个脚本有一系列的task,基本上就是:
compile, source code analytics, unit test, generate reports, generate javadoc,
package artifacts
这个,也是Java领域中经常使用的一个完整的过程。
有了这样一个脚本以后,我们开始配置我们的项目到QuickBuild中去,在QuickBuild中,我们配置一个configuration,然后设定我们的SCM repository,对应于我们的ant task,我们配置了一系列的step,用于完成整个过程。由于我们的测试需要跨平台,所以对应与同一个unit test的task,我们使用QuickBuild的分布式的step功能,使之在不同平台上可以进行测试,这一点也是使用CI Server的一个好处吧。
对应于这个configuration,我们配置了四个子configuration,分布是 Development Configuration, QA Configuration,Integration Configuration和Release Configuration。这几个configuration分别对应于我们开发过程的四个阶段,我们的每日构建都是在Development
configuration上的,所以我们配置为每日一次,而对于其它三个则不做自动的构建。因为我们是通过Promote来做的。对于 Development Configuration,我们没有对SCM自动打Label,而对于其它的,我们则对每一个Build自动对SCM进行打Label。
有了这些以后,开发工作开始了,我们每天的代码在下班前都提交到subversion里去,第二天,Development Configuration就自动的编译完成了,并且发送通知给我们。我们通常会会开一个Morning Meeting,首先我们会到在QuickBuild的页面上,看到昨天有哪些个改动,测试的状况,比如说哪些测试修正了,哪些测试还没有被修正,哪些 source code没有通过代码检查。然后我们会点到具体的报告中去分析,这些报告都可以很容易的打开source code,我们可以直接在上面对各个改动做code review。通常这个工程耗时约30分钟结束。
经过这样开发之后一段时间,我们的功能很多已经就绪,就可以提交给QA作test了,由于当日的构建可能失败,或者不是我们特别想给QA的,那么我们会选择之前几日的一个好的build做 Promote,这个promote就会自动触发QA Configuration去做build,QA Configuration的build做完以后,就会发送一个邮件通知QA Lead,这封邮件里QuickBuild会把所有与上一个QA build的changes都列出来,这样他就知道我们这个版本里增加了什么功能,修正了什么bug。
再如此经过几个迭代后,我们开发组和QA组一致认为功能基本实现了,bug也不多了,于是就由QA的Lead做一个Promote,触发Integration Configuration不build一个大版本交给客户,做VOC
(Voice of Customer),听却客户的意见,如果客户没有什么易见的话,那么就会在Integration Configuration上做一个Promote到Release Configuration上去。
通过这样做,我们基本上可以很容易的知道每一个版本之间有什么变化,甚至我们可以很容易的重新build出任何一个时间点上的版本。而且,我们基本上无需操心什么时候给SCM打什么样的Label,因为对于我们而言,我们需要看到的只是每一个版本的build。而如果用subversion来管理的话,也许你也可以通过命令来列出在SCM中各个版本的变化,但是如果有一天,你头昏忘记打label的话,或者打错label的话,也许要找到这个问题就不是那么容易了。又也许,你可以通过一系列的命令来完成这里提到的所有功能,但是我是懒惰的,而且很容易做错事情,所以我觉得如果机器可以完成的话,就让机器去做吧。
(四)基于Ant搭建敏捷开发过程中的持续集成环境
持续集成(CI)是敏捷开发过程中至为关键的一个环节,在每个迭代开发周期中,合理地对软件产品进行持续集成,将有效协调软件编码,测试以及版本发布各个团队的工作进程,降低软件开发风险,对客户需求做出最及时有效的反馈。Apache Ant 提供了丰富的核心任务以及扩展任务来完成持续集成过程中的各项工作,同时开源社区 Ant-Contrib提供的 Ant 任务更是大大增强了 Ant 的可编程性,使得 Ant 有能力完成更为复杂的逻辑操作。本文中将展示一个典型的 Web 2.0 应用在敏捷开发过程中的持续集成环境,并展示每个部分如何由 Ant 来具体实现。
1.基本的持续集成环境
一个持续、稳定的构建是整个持续集成过程中的关键。在每个迭代周期的开发过程中,软件开发团队应当及时将最新的功能代码进行提交和构建,以便使软件测试团队能够进行功能或系统测试,及早发现缺陷并尽快解决。同时,在相应迭代周期的后期,版本发布团队应该能够获取经过验证后的最新的产品构建,并将其打包成可交付产品或进行线上产品的更新,交由产品的项目关系人或最终客户进行使用,确保客户需求与软件产品一致。目前在 Web 2.0 应用的敏捷开发过程中多采用这样一种集成环境,以满足 Web 2.0 应用最为典型的“Always Beta”特性。
下图展示了一种基本的持续集成环境的拓扑结构。
图 1. 持续集成环境的拓扑结构
持续构建服务器上的 Ant 脚本首先从源代码管理库获取最新的代码,并按照特定的构建策略执行构建,比如在固定时间触发每日构建,而后将构建结果自动上传至 FTP 服务器用以保存和分发;功能测试 (FVT) 或系统测试 (SVT) 环境则包括了测试服务器和测试数据等信息,其上的 Ant 脚本则负责从 FTP 服务器获取最新的构建,提取更新所需的产品代码(如果需要,还要提取必要的数据库更新脚本,完成数据库架构的重构)来完成测试环境的更新,而后调用测试脚本进行测试并产生测试报告;产品环境则是面向终端用户的产品运行环境,其上的 Ant 脚本可以将通过测试的构建生成可交付的产品或线上产品更新包,这一过程常伴随产品版权信息验证、产品包压缩以及产品部署等动作。
2.Ant 如何帮助持续集成
Ant 作为 Java 开发领域应用最为广泛的自动构建工具,不仅可以帮助开发团队实施每日构建生成构建包,更支持在此构建包基础之上,生成测试团队和版本发布团队所需要的构建包以完成后续的产品测试与发布工作,最终使得整个迭代周期过程的产品集成实现自动化。本文不会对 Ant 的基本概念和所有任务进行逐一介绍,而是将作者在实践过程中认为对持续集成有所帮助的概念和任务加以解释阐明,以期读者更好地了解 Ant 的能力,并加以灵活的运用。
3.<Ant> 与 <Antcall>(<AntFetch> 与 <AntCallback>)
<ant> 任务提供了在一个构建脚本内调用外部脚本特定目标(target)的能力,这种能力可以很好的帮助我们管理整个持续集成过程。特别是在有多个项目构建需求的情况时,设计一个独立的控制脚本,借助 <ant> 任务使其通过调用不同的项目构建脚本完成整体项目的集成,这样做的一个最明显好处是使我们可以快速的适应项目变动,符合随需应变(On Demand)的开发模式。
<antcall> 任务区别于 <ant> 任务之处在于,其只能调用同一个构建脚本之内的构建目标,他所提供的是对一个构建脚本自身的清晰管理。以往依赖于 depends 属性的方式使得我们很容易迷失在复杂的目标依赖关系中,而使用 <antcall> 则能够将每个构建脚本的任务以显式的、易修改的方式呈现给项目构建者。
<antfetch> 和 <antcallback> 是 ant-contrib 开源项目提供的扩展任务,是增强版本的 <ant> 和 <antcall> 任务,他们不仅具备前二者的基本能力,还可以返回外部脚本或同脚本其他目标中的属性,可类比编程语言中带返回值的方法调用。
4.Available 与 Condition
在构建过程中,构建脚本不可避免地会对许多外部资源(文件,目录,URL 等)进行访问甚至修改,而为了能够有效的对这些资源进行操作,所需做的第一步通常是验证资源的可用性。<available> 任务可以帮助我们对各种外部资源进行判断,通过设置相应的属性来表明判断结果,进而引导后续的构建操作。
另外,在某些情况下,构建脚本或许需要对多个资源同时进行判断而不仅仅是单个资源。<condition> 任务通过支持丰富的内嵌标记(nested element),如 <and>/<or>/<xor> 等,具备了对资源进行更加复杂的逻辑判断能力。
5.For 与 If
当我们使用 Ant 脚本编写一些较为复杂的逻辑功能,比如循环和流程判断时,自然希望 Ant 能支持这种编程能力。然而 Ant 核心任务中并没有提供 <if> 任务,只是在 <target> 任务的属性中支持 if 属性,比如 <target name="build-module-A" if="module-A-present"/>,即表示只有 module-A-present 属性存在才执行 build-module-A 目标。但是,必须注意的一点是,这里的 if 并不是判断 module-A-present 属性是否设置为特定值,而仅仅是检查该属性是否被设置了,因而其可编程性并不是很强。
Ant-contrib 为 Ant 提供了与通常所使用的编程语言功能相同的 <if> 和 <for> 任务,在构建过程中灵活运用这两个任务,将大大增强 Ant 对逻辑操作的控制能力,这其实就是一种基于 XML 脚本的编程。
在具体的实践过程,有一点需要特别注意:在使用 <for> 任务的过程中,如果我们期望在循环体内使用一个变量,而非 Ant 的 property,则需借助由 ant-contrib 提供的 <variable> 任务(ant-contrib 任务)来实现。尽管这与 property 的值一经设置便无法改变的设计原则相抵,但有时能够给构建脚本很大程度上的灵活性。
6.Replace 与 ReplaceRegExp
在由开发构建包向产品构建包转变的过程中,替换与开发环境相关的属性值是主要工作之一 , 比如我们不能假设用户会将 JDK 安装在与开发环境相同的路径下,这时便可以使用 ant 的 <replace> 任务,<replace> 任务可以针对特定的字符串的执行替换操作。不仅如此,利用 Ant 扩展任务所提供的 <replaceRegExp> 任务,还可以实现基于正则表达式的替换。
例如,要将 test.bat 文件中的行首“java”字符串替换为“../../java/bin/java”而不影响其他“java”字符串,可以使用如下 ant 脚本:
<replaceregexp file="test.bat" byline="true" match="^java " replace="../../ java/bin/java " /> |
7.Filterchain 与 Mapper
Filterchain 和 Mapper 是在集成脚本中经常用到的 ant 概念。Filterchain 增强了面向数据传输的 ant 任务的能力,如 Concat,Copy,Loadproperties 和 Move,借助于各种不同功能的 filter,使得这些任务具备了数据筛选和处理的能力,非常类似于 Unix 系统中的管道的概念。
例如,要实现将 A 文件夹复制到 B 文件夹,同时对 B 文件夹中所有 jsp 文件的文件头添加 copyright.txt 文件内容的任务,可以使用如下 ant 脚本。
<copy todir="${B}"> <fileset dir="${A}" includes="*.jsp"/> <filterchain> <concatfilter prepend="copyright.txt"/> </filterchain> </copy> |
Mapper 则常出现于 Copy,Move 或 Unzip 任务中,它的作用在于为这些任务增加指定输出文件的能力,使得我们不仅可以通过 <fileset> 来指定源文件集,更可以通过各种不同功能的 mapper,来实现重新命名输出文件文件名或更改输出文件目录结构的能力,这在构建持续集成环境中起到了极为灵活的作用,很好的理解这两个概念有助于写出简单而功能全面的 ant 脚本。
8.Taskdef
另外,在一个复杂的持续集成环境中,我们不可避免地会涉及一些商业产品或者开源项目来搭建整个环境,比如使用 CVS,SVN 或 IBM ClearCase作为项目源代码库,使用 Apache Tomcat,IBM WebSphere Application 作为测试或产品环境的部署服务器,使用 LiquiBase,DBdeploy 作为产品数据库的持续重构工具等。而 Ant 借助其易扩展的特性,对所有这些工具提供了很好的支持,外部工具的提供者只要实现特定的 Ant 任务接口,就可以提供自定义的 Ant 任务,我们只需要通过 <taskdef> 任务引入这些特定的 Ant 任务,便可以实现与这些工具的连接,实现通过 Ant 脚本来管理整个集成环境的目的。
9.实现一个基本的持续集成环境
在一个典型的线上 Web 2.0 应用的迭代开发周期中,持续集成通常涉及构建、部署、测试和上线等一系列动作,而这些动作能够自动运行的前提是获取各自需要的产品包(比如基于 Java EE 的产品都须提供的 WAR 或 EAR 文件)。因此,在构建服务器上调用一个综合性的 Ant 构建脚本(清单 1),产生其它动作所需要的产品包,则成为整个持续集成过程中最为核心的一步。
清单 1. 产生其它动作所需要的产品包
<?xml version="1.0" encoding="UTF-8"?> <project name="SampleOverall" basedir="." default="fetch_Code"> <property file="SampleOverall.properties" />
<taskdef name ="teamFetch" classname="com.ibm.team.build.ant.task.TeamFetchTask" /> <taskdef name ="teamAccept" classname="com.ibm.team.build.ant.task.TeamAcceptTask" />
<tstamp><format property="build.time" pattern="yyyy-MM-dd$hh-mm-ss" /></tstamp>
<target name="perform_DailyBuild"> <antcall target="generate_SmokeTest_Package"/> <antcall target="mail"/> </target>
<target name="perform_FVTBuild"> <antcall target="generate_FVTTest_Package" /> <antcall target="upload_to_FTP" /> </target>
<target name="perform_ProductBuild"> <antFetch dir="${basedir}" antfile="checkLicense.xml" target="checkLicense" return="reportFile" /> <available property="reportFile_exist" file="${reportFile}"/> <fail message="Unlicensed file found, please check ${reportFile}" if="${reportFile_exist}"/> <antcall target="generate_Product_Package" /> <antcall target="upload_to_FTP" /> </target>
<target name="fetch_Code"> <teamAccept repositoryAddress="${repositoryAddress}" userId="${userId}" password="${password}" workspaceName="${workspaceName}" verbose="true" /> <teamFetch repositoryAddress="${repositoryAddress}" userId="${userId}" password="${password}" workspaceName="${workspaceName}" destination="${destination}" verbose="true" /> </target>
<target name="generate_SmokeTest_Package" depends="fetch_Code"> <!-- may also include other project,like SampleApp2, SampleApp3 --> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="war" inheritAll="false" /> </target>
<target name="generate_FVTTest_Package" depends="fetch_Code"> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="ear_FVT" inheritAll="false" /> </target>
<target name="generate_Product_Package" depends="fetch_Code"> <ant dir="${SampleApp1.dir}" antfile="build.xml" target="product" inheritAll="false" /> </target>
<target name="upload_to_FTP"> <zip destfile = "${BuildPackage}/SampleApp1.zip" basedir="${BuildPackage}" />
<ftp action="mkdir" server="${FTPAddress}" userid="${FTPUserName}" password="${FTPPassword}" remotedir="${FTPSharedFolder}/${build.time}"/> <ftp server="${FTPAddress}" userid="${FTPUserName}" password="${FTPPassword}" remotedir="${FTPSharedFolder}/${build.time}"> <fileset file="${BuildPackage}/SampleApp1.zip" /> </ftp> </target>
<target name="mail"> <mail mailhost="${MailServer}" mailport="${MailServerPort}" subject="Build Report Mail" tolist="${MailList}" messagemimetype="text/html" messagefile="mailcontent.html"> <from address="${fromMailAddr}" /> </mail> </target> </project> |
不难看出,<antcall> 任务通过调用不同的任务组合达到了为不同构建目的提供不同构建动作的目的,其中包括对“冒烟”测试,功能测试以及产品环境安装的特定支持,而各个环境所需要的产品包也因 <ant> 任务目标的不同而不同,这种松散组合的方式为今后脚本的维护和更新提供了良好的基础。
在为产品环境提供产品包(perform_productBuild)的目标中,<antFetch> 扩展任务通过调用外部的 checkLicese.xml 脚本来对产品进行版权核查,任何没有版权信息的文本文件都将被记录到 reportFile 中。虽然类似的这种功能可以使用多种脚本语言来方便的实现,比如 Python 和 Ruby 等,但这里给出了基于 Ant 的实现,以更好的展示 Ant 脚本的灵活性和可编程性。清单 2 是使用 Ant 实现版权信息检查的部分脚本。
清单 2. 使用 Ant 实现版权信息检查的部分脚本
<target name="checkLicense" > <for list="${scanFolderList}" param="folderList"> <sequential> <for list="${scanFileType}" param="fileType"> <sequential> <for param="file"> <path> <fileset dir="@{folderList}" includes=**/*.@{fileType}> <not> <contains text="${licenseFragment}" /> </not> </fileset> </path>
<sequential> <echo file="${reportFile}" message="@{file},${line.separator}" append="true" encoding="UTF-8"/> </sequential> </for> </sequential> </for> </sequential> </for> </target> |
对于测试环境和产品环境而言,获取产品包并自动的进行产品部署是两者共同的首要工作,而这个过程中所面临的主要问题通常涉及不同操作系统的脚本移植性问题。幸好,Ant 具备了良好的跨平台能力,我们不必为不同的部署环境(Windows 或 Linux)去编写不同的部署脚本,只需将精力集中于产品包的获取和针对不同应用服务器的部署即可,清单 3 展示了如何从 FTP 服务器获取产品包,并自动发布于 IBM WebSphere 应用服务器的过程。
清单 3. 获取产品包并自动部署
<?xml version="1.0" encoding="UTF-8"?> <project name="SampleProductEnv" basedir="." default="updateProduct">
<taskdef name="wsadmin" classname="com.ibm.websphere.ant.tasks.WsAdmin"/> <taskdef name="wsStopServer" classname="com.ibm.websphere.ant.tasks.StopServer"/> <taskdef name="wsStartServer" classname="com.ibm.websphere.ant.tasks.StartServer"/>
<target name="getFromFTP" > <ftp action="get" server="${FTPAddr}" userid="${FTPUsr}" password="${FTPPasswd}" remotedir="${product_FTP}"> <fileset dir="${basedir}/FTPDownload"> <include name="production.*"/> </fileset> </ftp> </target>
<target name="generateUpdatePack" depends="getFromFTP"> <unzip src="${basedir}/FTPDownload/Production.zip" dest="${basedir}/Production"> <globmapper from="code_build" to="app1"/> </unzip> </target>
<target name="updateProduct"> <wsadmin script="${basedir}/updateApp1.py" user="${MMC_user_name}" password="${MMC_user_password}" conntype="NONE" failonerror="yes"> <arg value="${update_files_location}/app1/WEB-INF/web.xml" /> </wsadmin>
<antcall target="stop-server" />
<copy todir="${product_Home}" overwrite="true"> <fileset dir="${update_files_location}/app1 "> <exclude name="**/web.xml"/> </fileset> </copy>
<antcall target="start-server" /> </target>
<target name="start-server"> <wsStartServer server="server1" noWait="false" trace="true" username="${MMC_user_name}" password="${MMC_user_password}" failonerror="yes"> </wsStartServer> </target>
<target name="stop-server"> <wsStopServer server="server1" noWait="false" trace="true" username="${MMC_user_name}" password="${MMC_user_password}" failonerror="yes"> </wsStopServer> </target> </project> |
10.结束语
本文首先介绍了一种在敏捷开发环境中最基本的持续集成环境,然后结合作者自身实践,讲述了几种能够为持续集成提供重要支持和能力的 Ant 任务及概念,最后通过示例性 Ant 脚本片段展示如何使用 Ant 脚本来快速地搭建这样一种环境。
11.参考资料
学习
- Apache Ant 用户手册,从这里可以了解到 Ant 相关的详细内容。
- Ant-Contrib Task, 从这里了解更多 Ant-contrib 任务。
- 查看 敏捷专区,了解更多有关敏捷开发相关知识。
- 查看 IBM WebSphere Application Server 信息中心,获取更多对 WAS 对 Ant 所提供的支持。
- “敏捷软件开发基础:持续集成环境的构建”(developerWorks,2005 年 6 月):本文中,作者将介绍如何构建持续集成所需要的环境。
- “实现持续集成”(developerWorks,2005 年 12 月):本文是对持续集成的概念和实践的一个介绍。
- developerWorks Java 技术专区:查找数百篇有关Java 编程各方面的文章。
获得产品和技术
- 下载 IBM 软件试用版,体验强大的 DB2®,Lotus®,Rational®,Tivoli®和 WebSphere®软件。
讨论
- 查看 developerWorks 博客 的最新信息。
(五)持续集成的深层理解与相关问题研究
1.持续集成与敏捷编程
在敏捷领域中,测试驱动和持续集成被称为敏捷编程的两大基石,于是乎,很多人的概念里就是持续集成是为了实现敏捷编程的。这是一个错误的认识。实际上,早于敏捷编程概念的提出,持续集成作为一个best practice就已经被很多公司采用了,只不过作为一个概念,则是由Martin为了推进敏捷所倡导并由此风靡起来。持续集成本身只是一种 practice,并不被什么开发模型所限制,在任何一种开发模型中都可以采用,也可以运行得非常理想。
2.持续集成还是阶段集成
有很多人说,我不做持续集成,照样工作的很好。因为我们一个(小)阶段出一个版本,照样控制得非常好。我得恭喜你,首先持续集成也好,阶段集成也罢,你做了,做了就好,比没有做要好很多,也使你的项目管理上了轨道了。这两者之间的区别仅是频率而已。
那么究竟那种方式更加理想,更加符合项目的开发和管理呢?其实这个问题Steve McConnell在他那本获得Jolt大奖的书《Code Complete》(代码大全)里有过回答了。他说:对于一个微型程序来说,阶段式的集成或许是最佳方法。何谓微型程序,他说就是那种两三个类的程序,而你又很走运的话,那么阶段式集成就可以是你的最佳方法了。当然,这位老兄是个老美,我们也都知道老外嘛,都比较笨一点,不会转弯一点,所以呢,他说微型程序,对于我们拥有5000年文明的中国人说,可以再扩大点吧,对于一 个小型项目,就是那种二三十个类的项目,也许使用阶段集成也不会出啥子问题吧。
3.对持续集成好处的理解
很多人肯定非常不苟同我的看法,他们认为即使没有做持续集成,甚至没有做阶段集成,但是项目一样按时的完成,甚至提前完成,而且照样完成的非常理想,老板满意,客户满意,夫复何求?而做持续集成,无非就是动不动收到一封邮件,说这个build成功了,那个build失败了,不过就是持续编译罢
了,我自己打个命令编译一下,不就知道了吗?要做个daily build,我还要颠颠的去set up,还要花力气去配置,效果也不见得好到什么地方去。
对于这样一些问题,我想首先我们还得搞清楚,究竟为什么我们要去做持续集成,持续集成究竟可以给我们带来什么好处。同样在《Code Complete》里提到了,对于持续集成(在书中,Steve McConnell使用Incremental Integration的术语)有以下几点好处:
易于定位错误
也就是当你的持续集成失败了,说明你新加的代码或者修改的代码引起了错误,这样你很容易的就可以知道到底是谁犯了错误,可以找谁来讨论。
及早在项目里取得系统级的成果
因为代码已经被集成起来了,所以即使整个系统还不是那么可用,但至少你和你的团队都已经可以看到它已经在那了。
改善对进度的控制,改善客户关系
这点非常明显,如果每天都在集成,当然每天都可以看到哪些功能可以使用,哪些功能还没有实现。如果你是程序员,你不用在汇报任务的时候说我完成了多少百分比而烦恼,而如果你是项目经理的话,那么你也不再烦恼程序员说完成了编码的50%到底是个什么概念。
更加充分地测试系统中的各个单元
这也是我们常讲的Daily Build与Smoke Test相结合带来的绝大好处。
能在更短的时间里建造整个系统
这点恐怕要你实施以后才能得出结论。就我们而言,持续集成并没有为每个项目都缩短时间,但却比没有实施时,项目更加可控,也更加有保证。
随着时间的推移,持续集成带来的更多好处,也逐渐被认识到了,比如说:
有助于项目的开发数据的收集 比如说,项目代码量的变化,经常出错的Tests,经常出错的source code,等等。
与其它工具结合的持续代码质量改进
如与CheckStyle, PMD, FindBugs, Fxcop等等等等的结合。
与测试工具或者框架结合的持续测试
如与xUnit,SilkTest, LoadRunner等等的结合。
便于Code Review
在每个build里,我们都可以知道与前一个build之间有什么改动,然后针对这些改动,我们就可以实施Code Review了。
便于开发流程的管理
比如说,要把一个开发的build提交给测试组作测试,测完满意了,再提交到发布组去发布。
4.持续集成应该自动化什么
我们要以尽可能少的成本来获得尽可能多的价值。这就要考虑哪些自动化是必要的。Jez Humble提到至少有六点要做到自动化,它们分别是(1)自动化的运行测试;(2) 自动产生可部署的二进制成品;(3) 自动将成品自动部署到近似生产环境;(4) 自动为CodeBase打上标签;(5) 自动运行回归测试;(6)自动生成度量报告。
5.只有持续集成服务器是远远不够的
正如Jez Humble所说,CruiseControl和其它的CI工具本质上只不过是一个定时器,时间一到,做你让它做的事情。所以,必然要有其它工具与其结合,方显持续集成的本色。这些工具又是什么呢?想测试的话,你就要用一些测试工具,如JUnit,JWebUnit,Selenium等等;想检查代码标准的话,你就要用checkstyle等代码规范检查工具;想要了解测试覆盖率的话,你可能就要用到JCoverage啦。当然,想得到二进制文件,就要用到Ant,Make之类的工具啦。
6.最重要的事:实践与反思
也许这些东西大家都知道,而且有些人可能已经实践过。无论这些实践的结果是怎样的,一定不要忘记总结和反思。如果这些实践成功了,不要把它归功于这个工具,而是要总结一下为什么会成功,如果你愿意的话,还可以和大家分享一下。如果这些实践失败了,也不要把它归功于这个工具,而是要反思一下,是否正确地使用了这个工具,团队成员是否都喜欢这个工具,为什么?
7.跌跌撞撞的持续集成之路(分享持续集成经验)
网站链接:http://cd.qq.com/a/20090910/000715.htm
posted on 2014-08-05 23:31 pengdaijun 阅读(369) 评论(0) 编辑 收藏 举报