【Hello CC.NET】自动化发布时 Web.config 文件维护
在 《【Hello CC.NET】CC.NET 实现自动化集成》 的 HellowWorld 中经实现:
1.获取源码
2.编译项目
3.集成测试
4.Ftp发布项目
5.创建安装包
6.邮件通知
在方案落地的过程中,FTP 上传开发自测环境(或测试环境)后,仍然需要人手来修改相关配置(比如 Web.config)。
假设项目的 Web.config 配置如下:
<appSettings> <add key="OrgDB" value="xxx_dev_db"/> <add key="BusinessDB" value="xxx_DEV_DB"/> <add key="RedisCache" value="192.168.10.111:6379"/> <add key="SSOService" value="http://192.168.10.111/DEV/V1.1.9/SSOService.svc"/> <add key="Coder" value="蔡海华"/> </appSettings>
在发布到测试环境之前,我们需要把 _dev_db 替换为 _test_db,192.168.10.111:6379 替换为 192.168.10.112:6379 ,http://192.168.10.111/DEV/V1.1.9/SSOService.svc 替换为 http://192.168.10.111/Test/V1.1.9/SSOService.svc(忽略大小写)。
一、用 bat 脚本实现文件拷贝和web.config内容替换
1.文件拷贝
为了不影响旧的环境,我们新建一个文件夹 PackageDirectory 来保存修改后的文件。
文件拷贝用批处理来实现:新建一个 txt 文件,输入以下内容,另存为 DFCopier.bat(directories & files copier),copyfrom 和 copyto 由参数方式传入。
@echo off set from=%1 set to=%2 ::rd /q/s "%to%"&&md "%to%" dir /a /b %to%|findstr .>nul 2>nul && goto havefiles || goto nofiles :havefiles rem echo 有文件 goto end :nofiles rem echo 没有文件 XCOPY %from% %to% /c/q/e/r :end
2.文件内容替换
新建一个 txt 文件,输入以下内容,另存为 FCReplacer.bat(file content replacer),directory , file, from, to 由参数方式传入。
@echo off set dir=%1 set file=%2 set from=%3 set to=%4 setlocal enabledelayedexpansion pushd %dir% for /f "tokens=1,2* delims=:" %%i in ('findstr /n ".*" %file%') do ( set txt=%%j if "!txt!" == "" ( echo.>>%dir%%file%.tmp ) else ( echo !txt:%from%=%to%!>>%dir%%file%.tmp ) ) move /y %dir%%file%.tmp %file% :end
3.配置 ccnet.config
在 tasks 节点中添加以下两个子节点。假设一个最简单的场景:替换数据库命名 xxx_dev_db 为 xxx_test_db。
<!-- 复制文件到 Package 目录 --> <exec> <executable>C:\CC.NET\Server\DFCopier.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PublishDirectory C:\CC.NET\Server\Test\PackageDirectory</buildArgs> </exec> <!-- 替换 Package 目录下的 WebConfig 文件 --> <exec> <executable>C:\CC.NET\Server\FCReplacer.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PackageDirectory\WcfService Web.config _dev_db _test_db</buildArgs> </exec>
ftp 和 package 部分修改如下:
<!--ftp发布Wcf服务到开发环境--> <ftp> <serverName>127.0.0.1</serverName> <userName>admin</userName> <password>admin</password> <action>UploadFolder</action> <ftpFolderName></ftpFolderName> <localFolderName>C:\CC.NET\Server\Test\PackageDirectory\WcfService</localFolderName> <recursiveCopy>true</recursiveCopy> <timeDifference>1</timeDifference> </ftp> <package> <name>Lib.sln</name> <compression>9</compression> <manifest type="defaultManifestGenerator" /> <packageList> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\*.svc" targetFolder="WcfService" /> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\*.Release.config" targetFolder="WcfService" /> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\bin\*.dll" targetFolder="WcfService\bin" /> <!--<packageFolder sourceFolder="C:\CC.NET\Server\Test\PublishDirectory\WcfService" targetFolder="WcfService" fileFilter="*.*" flatten="false" includeSubFolders="false" />--> </packageList> </package>
4.测试
Forcebuild 得到以下结果(只截取了 DFCopier.bat 和 FCReplacer.bar 相关的部分):
打开 Web.config 文件却发现中文会变成乱码。
二、自己实现文件内容替换功能
批处理的方式在中文面前显得有点苍白无力,另外一些更复杂的场景是无法简单地 replace 来解决。一番 Google 折腾后我决定自己写一个 replacer,支持正则来处理可能出现的复杂需求。
1.实现 replacer
新建一个项目,命名为 FCReplacer。实现的代码 100 行不到。编译得到 FCReplacer.exe
class Program { static void Main(string[] args) { var file = GetFileName(args); var replacers = GetReplaers(args); if (file == null || replacers == null || replacers.Count() == 0) return; var txt = File.ReadAllText(file); Console.WriteLine(string.Format("Replace content of file({0})", file)); foreach (var replacer in replacers) { Console.WriteLine(string.Format("Replace {0} to {1}", replacer.From, replacer.To)); txt = replacer.Replace(txt); } File.WriteAllText(file, txt); } static string GetFileName(string[] args) { string filename = null; var fileRegex = new Regex("^/file=(=?.*)$", RegexOptions.Compiled); foreach (var arg in args) { var match = fileRegex.Match(arg); if (match.Groups.Count == 2) { filename = match.Groups[1].Value.Trim(); break; } } return filename; } static IEnumerable<Replacer> GetReplaers(string[] args) { var list = new List<Replacer>(); var replacerRegex = new Regex("^/from=(=?.*)/to=(=?.*)$", RegexOptions.Compiled); foreach (var arg in args) { var match = replacerRegex.Match(arg); if (match.Groups.Count == 3) { var from = match.Groups[1].Value.Trim(); var to = match.Groups[2].Value.Trim(); list.Add(new Replacer() { From = from, To = to }); } } return list; } class Replacer { public string From { get; set; } public string To { get; set; } public string Replace(string txt, RegexOptions regexOption = RegexOptions.IgnoreCase) { txt = Regex.Replace(txt, this.From, this.To, regexOption); return txt; } } }
2.配置 ccnet.config
把文件替换的批处理部分改为 FCReplacer.exe。替换的逻辑通过参数方式传入。假设三个更复杂的场景:
(1) 替换数据库名 xxx_dev_db 为 xxx_test_db
(2)替换 redis 缓存服务的地址 192.168.10.111:PORT 为 192.168.10.112:PORT,端口设置不变
(3)替换 SSO 服务地址,版本号不变
<!-- 复制文件到 Package 目录 --> <exec> <executable>C:\CC.NET\Server\DFCopier.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PublishDirectory C:\CC.NET\Server\Test\PackageDirectory</buildArgs> </exec> <!-- 替换 Package 目录下的 WebConfig 文件 --> <!--<exec> <executable>C:\CC.NET\Server\FCReplacer.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PackageDirectory\WcfService Web.config _dev_db _test_db</buildArgs> </exec>--> <exec> <executable>C:\CC.NET\Server\FCReplacer.exe</executable> <buildArgs> /file=C:\CC.NET\Server\Test\PackageDirectory\WcfService\Web.config /from=_dev_db/to=_test_db /from=http://192.168.10.111/Dev/V(=?\d+(.\d+)*)/SSOService.svc/to=http://192.168.10.111/Test/V$1/SSOService.svc </buildArgs> </exec>
3.测试
Forcebuild 得到以下结果(只截取了 DFCopier.bat 和 FCReplacer.exe 相关的部分):
打开 Web.config 文件,三个替换逻辑都已经实现,中文也不再是乱码。
完整的 ccnet.config 配置如下:
<cruisecontrol xmlns:cb="urn:ccnet.config.builder"> <project name="Lib.Sln"> <!--标签--> <labeller type="dateLabeller"/> <artifactDirectory>C:\CC.NET\Server\Test\ArtifactDirectory</artifactDirectory> <!--项目的目录--> <workingDirectory >C:\CC.NET\Server\Test\WorkingDirectory</workingDirectory> <!--自动构建结果的查看地址--> <webURL>http://vw-caihaihua/CC/server/local/project/Lib.Sln/ViewProjectReport.aspx</webURL> <!--自动运行时间间隔--> <triggers> <!--源码修改触发--> <intervalTrigger seconds="10" buildCondition="IfModificationExists " /> <!--每日构建--> <scheduleTrigger time="19:00" buildCondition="ForceBuild"> <weekDays> <!--<weekDay>Sunday</weekDay>--> <weekDay>Monday</weekDay> <weekDay>Tuesday</weekDay> <weekDay>Wednesday</weekDay> <weekDay>Thursday</weekDay> <weekDay>Friday</weekDay> <!--<weekDay>Saturday</weekDay>--> </weekDays> </scheduleTrigger> </triggers> <!--对源码修改延迟处理时间间隔--> <modificationDelaySeconds>30</modificationDelaySeconds> <maxSourceControlRetries>5</maxSourceControlRetries> <!--源代码管理(SVN)--> <sourcecontrol type="svn"> <trunkUrl>https://vw-caihaihua/svn/Test/trunk/</trunkUrl> <executable>C:\Program Files (x86)\VisualSVN Server\bin\svn.exe</executable> <workingDirectory>C:\CC.NET\Server\Test\WorkingDirectory\</workingDirectory> <username>ci</username> <password>123456</password> </sourcecontrol> <tasks> <!--<devenv> <solutionfile>C:\CC.NET\Server\Test\WorkingDirectory\Lib.sln</solutionfile> <configuration>Debug</configuration> </devenv>--> <!--清理解决方案--> <msbuild> <executable>C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe</executable> <buildArgs>/t:clean /t:rebuild /p:configuration=debug</buildArgs> <workingDirectory>C:\CC.NET\Server\Test\WorkingDirectory</workingDirectory> <projectFile>Lib.sln</projectFile> <logger>ThoughtWorks.CruiseControl.MsBuild.XmlLogger,C:\Program Files (x86)\CruiseControl.NET\server\ThoughtWorks.CruiseControl.MsBuild.dll</logger> </msbuild> <!--运行UnitTest--> <exec> <executable>C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe</executable> <baseDirectory>C:\CC.NET\Server\Test\WorkingDirectory</baseDirectory> <buildArgs>/testcontainer:LibTest\bin\Debug\LibTest.dll</buildArgs> <buildTimeoutSeconds>6000</buildTimeoutSeconds> </exec> <!--发布Wcf服务到本机--> <msbuild> <executable>C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe</executable> <workingDirectory>C:\CC.NET\Server\Test\WorkingDirectory\WcfService</workingDirectory> <projectFile>WcfService.csproj</projectFile> <buildArgs> /t:ResolveReferences;Compile /t:_CopyWebApplication /p:Configuration=Release /p:WebProjectOutputDir=C:\CC.NET\Server\Test\PublishDirectory\WcfService /p:OutputPath=C:\CC.NET\Server\Test\PublishDirectory\WcfService\bin </buildArgs> </msbuild> <!--运行UnitTest--> <exec> <executable>C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\MSTest.exe</executable> <baseDirectory>C:\CC.NET\Server\Test\WorkingDirectory</baseDirectory> <buildArgs>/testcontainer:WcfServiceTest\bin\Debug\WcfServiceTest.dll</buildArgs> <buildTimeoutSeconds>6000</buildTimeoutSeconds> </exec> <!--启动 Asp.NET Development Server--> <!--<exec> <executable>C:\Program Files (x86)\Common Files\microsoft shared\DevServer\10.0\WebDev.WebServer40.EXE</executable> <buildArgs>/port:9999 /path:C:\CC.NET\Server\Test\PublishDirectory\WcfService</buildArgs> <buildTimeoutSeconds>6000</buildTimeoutSeconds> </exec>-->
<!--ADDED START--> <!-- 复制文件到 Package 目录 --> <exec> <executable>C:\CC.NET\Server\DFCopier.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PublishDirectory C:\CC.NET\Server\Test\PackageDirectory</buildArgs> </exec> <!-- 替换 Package 目录下的 WebConfig 文件 --> <!--<exec> <executable>C:\CC.NET\Server\FCReplacer.bat</executable> <buildArgs>C:\CC.NET\Server\Test\PackageDirectory\WcfService Web.config _dev_db _test_db</buildArgs> </exec>--> <exec> <executable>C:\CC.NET\Server\FCReplacer.exe</executable> <buildArgs> /file=C:\CC.NET\Server\Test\PackageDirectory\WcfService\Web.config /from=_dev_db/to=_test_db /from=http://192.168.10.111/Dev/V(=?\d+(.\d+)*)/SSOService.svc/to=http://192.168.10.111/Test/V$1/SSOService.svc a </buildArgs> </exec>
<!--ADDED END-->
<!--ftp发布Wcf服务到开发环境--> <ftp> <serverName>127.0.0.1</serverName> <userName>admin</userName> <password>admin</password> <action>UploadFolder</action> <ftpFolderName></ftpFolderName> <localFolderName>C:\CC.NET\Server\Test\PackageDirectory\WcfService</localFolderName> <recursiveCopy>true</recursiveCopy> <timeDifference>1</timeDifference> </ftp> <package> <name>Lib.sln</name> <compression>9</compression> <manifest type="defaultManifestGenerator" /> <packageList> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\*.svc" targetFolder="WcfService" /> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\*.Release.config" targetFolder="WcfService" /> <packageFile sourceFile="C:\CC.NET\Server\Test\PackageDirectory\WcfService\bin\*.dll" targetFolder="WcfService\bin" /> <!--<packageFolder sourceFolder="C:\CC.NET\Server\Test\PublishDirectory\WcfService" targetFolder="WcfService" fileFilter="*.*" flatten="false" includeSubFolders="false" />--> </packageList> </package> </tasks> <state type="state" directory="C:\CC.NET\server\CCState"/> <publishers> <!--标签备份(如果成功)--> <buildpublisher> <sourceDir>C:\CC.NET\Server\Test\WorkingDirectory</sourceDir> <publishDir>C:\CC.NET\Server\Test\HistoryVersion</publishDir> </buildpublisher> <modificationHistory/> <statistics/> <!--邮件通知--> <email mailhost="smtp.live.com" mailport="25" mailhostUsername="ci@XXXCompany.com" mailhostPassword="******" from="ci@XXXCompany.com" useSSL="TRUE" includeDetails="true"> <!--邮件标题配置--> <subjectPrefix>[CI@XXXCompany]</subjectPrefix> <subjectSettings> <!-- Success/Broken/StillBroken/Fixed/Exception--> <subject buildResult="Success" value="${CCNetProject} Build Successful: Label ${CCNetLabel}, last checkin(s) by ${CCNetModifyingUsers}.(at ${CCNetBuildDate} ${CCNetBuildDate})" /> <subject buildResult="Fixed" value="${CCNetProject} Build Fixed: Label ${CCNetLabel}, last checkin(s) by ${CCNetModifyingUsers}.(at ${CCNetBuildDate} ${CCNetBuildDate})" /> <subject buildResult="Broken" value="${CCNetProject} Broke: last checkin(s) by ${CCNetFailureUsers}.(at ${CCNetBuildDate} ${CCNetBuildDate})" /> <subject buildResult="StillBroken" value="${CCNetProject} Still Broken: last checkin(s) by ${CCNetFailureUsers}.(at ${CCNetBuildDate} ${CCNetBuildDate})" /> <subject buildResult="Exception" value="${CCNetProject} In Exception: Please check status of network / sourcecontrol.(at ${CCNetBuildDate} ${CCNetBuildDate})" /> </subjectSettings> <!--收件人配置--> <converters> <regexConverter find="$" replace="@XXXCompany.com"/> </converters> <modifierNotificationTypes> <notificationType>Failed</notificationType> <notificationType>Fixed</notificationType> </modifierNotificationTypes> <users> <user group="leader" name="ci.XXXCompany" address="ci@XXXCompany.com"/> <user group="developer" name="harvey.choi" address="harvey.choi@XXXCompany.com"/> <user group="tester" name="jolin" address="jolin.tang@XXXCompany.com"/> </users> <groups> <group name="leader"> <notifications> <!--Always/Success/Change/Fixed/Failed --> <notificationType>Change</notificationType> <notificationType>Exception</notificationType> </notifications> </group> <group name="developer"> <notifications> <notificationType>Success</notificationType> <notificationType>Fixed</notificationType> <notificationType>Failed</notificationType> </notifications> </group> <group name="tester"> <notifications> <notificationType>Fixed</notificationType> </notifications> </group> </groups> </email> <xmllogger/> </publishers> </project> </cruisecontrol>
到这里,一个简单场景的自动化发布部分已经实现。TODO 列表里仍须解决的几个主要问题:
1.Build 成功,人工测试未通过时,如何一键回滚
2.数据库的更新和回滚如何实现自动化