聲明:本文應用情境為TFS 2012,TFS 2013可在組建定義加掛PowerShell Script於建置前後執行,應比本文介紹的做法簡便。(請參考Franma的文章

使用TFS Build Service成功建置專案後,我們希望做到建置後自動上傳測試台或UAT驗收主機的目標。考慮過幾種做法,包含自訂建置流程範本、撰寫派送服務,最後決定將部署作業編寫放進csproj,工程最小,依賴性最低,較符合KISS原則。

之前對MSBuild的粗淺研究,知道csproj本身就是個MSBuild設定檔,我們可以在其中用Target包裝一連串動作,使用<Exec Command="">可以在其中執行XCOPY等DOS指令,並能指定執行條件Condition及時機。例如:AfterTargets="Build"代表在建置後執行、Condition="'$(ParamName)' == 'Y'"可以用參數控制執行與否。

由於Build Service與測試台/UAT主機間網路頻寬有限,若每次部署都複製整個網站目錄,耗時甚久(傳輸整個網站需數十分鐘),只複製有更新檔案是較有效率的做法。好用的Robocopy可使用/XO參數限定只複製存檔時間較新的檔案,是過去常用的部署工具。但遇到一個問題,預設Build Service每次會清空Workspace重新由TFS下載,導致網站所有的css、js、html、jpg/png/gif的檔案時間都被改成建置時間,Robocopy的檔案時間比對特性當場破功。

經過一番研究,找到一個組建定義 Clean Workspaces:它有三個選項,All/Outpus/None,選 All 會刪除輸出及原始碼資料夾,形同完全重新建置;選 Outputs 則刪除輸出資料夾保留原始碼資料夾,只下載上次建置後有修改的檔案(Incremental Get,遞增式下載);選擇 None 會只更新有修改檔案且保留輸出資料夾。

透過Incremental Get,在第一次建置之後,每次建置只會更新修改過的檔案會,便能實現部署時只複製更過的目標。(補充:TypeScript、SCSS每次建置時會重新產生js, min.js, css, min.css,故這部分較難避免重複更新)

在csproj中安裝Robocopy作業,會遇到幾項挑戰:

  1. Robocopy作業應限定Build Service建置時才執行
    關於這點,在<Target>加上Condition="'$(BuildingInsideVisualStudio)' == ''"可限定非使用Visual Studio建置時才執行。但更精準的做法可指定特定參數控制,例如:Condition="'$(DeployAction)' == 'Test'",配合MSBuild執行參數控制部署行為。
  2. Robocopy Exit Code問題
    Robocopy使用0以外的Exit Code傳回執行結果,與一般DOS程式Exit Code不等於0代表出錯的慣例不符,會被MSBuild誤判為執行失敗,針對此可加上IF %ERRORLEVEL% LSS 8邏輯,將Exit Code 0 - 7 轉為 Exit 0。
  3. <Exec Command="" >執行多行程式
    承2,Robocopy配合IF %ERRORLEVEL%需寫成兩行,MSBuild XML規則裡有項密技,在Command字串雙引號間直接換行就搞定了:(或使用&#xD;&#xA;代表換行符號也成)
    <Exec Command="Line1
    Line2
    Line3">
  4. 排除不想覆寫的檔案
    由於web.config、NLog.config在不同主機設定不盡相同,加上設定常包含敏感資料如連線字串、API網址等,不適合Check In TFS。故我們習慣部署時不覆寫config檔,視需求再手動修改。
    Robocopy有/X*參數可排除特定檔案,我選擇另一種做法,編譯後執行DEL將指定檔案刪除,但要記得加上/Q /F避開確認詢問及強制刪除唯讀檔。
  5. Build Controller執行身分
    由於Robocopy使用網路資料夾方式將檔案複製到遠端主機,最簡單的做法是以網域帳號當作Build Controller的執行身份,並授與該帳號存取遠端主機網路資料夾的權限。

開啟csproj,加入以下內容,MSBuild會在DeployAction參數等於Test時,於建置完成後刪除*.config檔,並用robocopy將較新的檔案複製到DeployTargetPath所指定的UNC位址。

  </ProjectExtensions>
  <Import Project="$(SolutionDir)\.nuget\NuGet.targets"
    Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
  <Target Name="DeployAction" Condition="'$(DeployAction)' == 'Test'" AfterTargets="Build">
    <Message Text="Delete config..." />
    <Exec Command="del $(WebProjectOutputDir)\*.config /Q /F"></Exec>
    <Message Text="Deploy to $(DeployTargetPath)..." />
    <Exec Command=
"robocopy &quot;$(WebProjectOutputDir)&quot; &quot;$(DeployTargetPath)&quot; /S /E /copy:DAT /XO
if %errorlevel% lss 8 exit 0 else exit %errorlevel%" Condition="'$(DeployTargetPath)' != ''">
    </Exec>    
  </Target> 

要啟用上述作業,必須指定DeployAction及DeployTargetPath兩個MSBuild參數,可在組建定義 MSBuild Arguments屬性指定。

每次Queue New Build時,也還有機會調整。

就這樣,每次使用建置時,Build Service便會一併將更新旳檔案部署到遠端主機上,總算實現部署自動化的夢想。


Comments

Be the first to post a comment

Post a comment