﻿# 在 Azure Pipelines 裡面正確使用 ROBOCOPY 複製檔案的方法

在命令列環境下執行程式，這個世界普遍有個共識，那就是應用程式的**結束狀態碼**(Exit Code)為 `0` 時，就會被視為是「沒有錯誤」的結果。任何非 0 的結束狀態碼，都代表有一定程度的錯誤發生。因此在 Azure Pipelines 或任何其他 CI 平台上，預設遇到應用程式回傳 Non-Zero 的結束狀態碼，就會自動報錯。誰知道 [ROBOCOPY](https://ss64.com/nt/robocopy.html) 原來複製成功，也會回傳非 `0` 的結束狀態碼！

### 原始的命令

以下就是我手動執行命令的腳本，測試過很多遍，是沒問題的：

```powershell
$SourceRoot = 'D:\Projects\MyProject'
$TomcatAppPath = 'C:\Program Files\Apache Software Foundation\Tomcat 8.5\webapps'

ROBOCOPY /E "$SourceRoot\web\build\war-tmp"   "$TomcatAppPath\web" /NP /NFL /NDL /NJH /NJS

ROBOCOPY /E "$SourceRoot\api\build\war-tmp"   "$TomcatAppPath\api" /NP /NFL /NDL /NJH /NJS
```

### 修改過的命令

```powershell
$SourceRoot = 'D:\Projects\MyProject'
$TomcatAppPath = 'C:\Program Files\Apache Software Foundation\Tomcat 8.5\webapps'

ROBOCOPY /E "$SourceRoot\web\build\war-tmp"   "$TomcatAppPath\web" /NP /NFL /NDL /NJH /NJS
if( $LASTEXITCODE -ge 8 ) {
    throw ("An error occured while copying to $TomcatAppPath\web. [RoboCopyCode: $($LASTEXITCODE)]")
} else {
    $global:LASTEXITCODE = 0;
}

ROBOCOPY /E "$SourceRoot\api\build\war-tmp"   "$TomcatAppPath\api" /NP /NFL /NDL /NJH /NJS
if( $LASTEXITCODE -ge 8 ) {
    throw ("An error occured while copying to $TomcatAppPath\api. [RoboCopyCode: $($LASTEXITCODE)]")
} else {
    $global:LASTEXITCODE = 0;
}
```

### 修改 Azure Pipelines 專用的命令

```powershell
$SourceRoot = '$(Build.SourcesDirectory)'
$TomcatAppPath = '$(Build.ArtifactStagingDirectory)\Apache\Tomcat85\webapps'

ROBOCOPY /E "$SourceRoot\web\build\war-tmp"   "$TomcatAppPath\web" /NP /NFL /NDL /NJH /NJS
if( $LASTEXITCODE -ge 8 ) {
    throw ("An error occured while copying to $TomcatAppPath\web. [RoboCopyCode: $($LASTEXITCODE)]")
} else {
    $global:LASTEXITCODE = 0;
}

ROBOCOPY /E "$SourceRoot\api\build\war-tmp"   "$TomcatAppPath\api" /NP /NFL /NDL /NJH /NJS
if( $LASTEXITCODE -ge 8 ) {
    throw ("An error occured while copying to $TomcatAppPath\api. [RoboCopyCode: $($LASTEXITCODE)]")
} else {
    $global:LASTEXITCODE = 0;
}
```

### 為什麼不容易發現問題

我個人實作 Pipelines 的方式，都是先將所有步驟寫成指令碼，然後再搬移到 Pipelines 執行。如果你可以手動透過命令執行成功，那麼套用到 CI/CD 肯定也沒問題！

為什麼我會沒發現 ROBOCOPY 複製成功時並非回傳狀態碼 `0` 的結果呢？因為我是這樣做的：

1.  先寫好指令，並手動測試個幾次
2.  確認指令沒問題，確認回傳狀態碼為 `0`
3.  設定 Azure Pipelines 並測試執行，得到回傳狀態碼為 `1`（見鬼啦～）
4.  重新用手動執行，確認回傳狀態碼為 `0`
5.  再測試一次 Pipeline 並得到回傳狀態碼為 `1`（見鬼啦～）
6.  無限鬼打牆... ♾

這是因為我每次手動部署的時候，其實在 `$(Build.ArtifactStagingDirectory)` 目錄下都已經有檔案，而 ROBOCOPY 只有一種條件會回傳狀態碼 `0`，那就是 `No errors occurred, and no copying was done.` (沒有錯誤發生，且沒有檔案被複製)。但是 Azure Pipelines 被觸發執行的時候，會清除所有 `$(Build.ArtifactStagingDirectory)` 目錄下的內容，這才發生這種鬼打牆的事件！

### 深入 ROBOCOPY 結束狀態碼

深入瞭解之後我才發現，原來 ROBOCOPY 的結束狀態碼只要是 `0` ~ `7` 都屬於正常結束，大於等於 `8` 的結束狀態碼才是有問題的。

ROBOCOPY 的狀態碼比一般命令列程式還複雜許多，他使用 bitmap 或稱 flags 來代表多種狀態的組合，基本的 flag 有以下這些，可以自由組合：

-   結束狀態碼 `0` 代表的意義是
    -   **No errors occurred**, and **no copying was done**. The source and destination directory trees are completely synchronized.
-   結束狀態碼 `1` 代表的意義是
    -   **One or more files were copied successfully**. (that is, new files have arrived).
-   結束狀態碼 `2` 代表的意義是
    -   **Some Extra files or directories were detected**. **No files were copied**. Examine the output log for details.
-   結束狀態碼 `4` 代表的意義是
    -   **Some Mismatched files or directories were detected**. Examine the output log. Housekeeping might be required.
-   結束狀態碼 `8` 代表的意義是
    -   Some files or directories **could not be copied**. (copy errors occurred and the retry limit was exceeded). Check these errors further.
-   結束狀態碼 `16` 代表的意義是
    -   **Serious error**. Robocopy did not copy any files. Either a usage error or an error due to insufficient access privileges on the source or destination directories.

### 後記

我曾經聽到在網路上有人說過：「不要跟我談什麼 CI/CD，你先手動執行個 100 遍再來跟我說話。」

以往各種人工作業，大家會在無數次**試誤**(try and error)的過程中，流失掉許多寶貴的經驗與知識，以致於錯誤不斷再犯，無法建立起十足的信心自動化。但是導入自動化建置與部署的過程中，由於把所有過程**指令化**，達到 IaC (Infrastructure as Code) 的效果，讓你可以對這些腳本進行版控，累積起大大小小的經驗，就算有錯誤，修掉之後也會不斷累積自信，讓公司的自動化越做越好。

沒有累積夠多的人工建置與部署經驗，在實現 CI/CD 的過程中，會遭遇到你想像不到的各種狀況。這些年許多客戶找我們幫忙建置 CI/CD 環境，客戶雖然不擅長自動化的過程，但是我們會在導入的過程中，幫助客戶釐清各種問題，過程中不但可以發現問題，還可以開始改變觀念。

一般公司大多只想直接購買解決方案來快速解決問題，但事實上，自動化的過程其實是在累積公司資產，把各種無形的知識，透過指令碼(Code)的方式，累積成公司長久可維護的有形知識。過程中會遇到許多挑戰，把冰山下方的所有問題一一浮現，同時也能讓公司的體質變的越來越好！

### 相關連結

-   [Robocopy "Robust File Copy" - Windows CMD - SS64.com](https://ss64.com/nt/robocopy.html)
-   [Robocopy Exit Codes - Windows CMD - SS64.com](https://ss64.com/nt/robocopy-exit.html)
-   [Robocopy in TFS Build PowerShell Step Reports Failure But Has No Error - Stack Overflow](https://stackoverflow.com/a/32351407/910074)
-   [azure devops - Robocopy fails when is used from TFS Builds - Stack Overflow](https://stackoverflow.com/a/54903746/910074)
-   [Predefined variables - Azure Pipelines | Microsoft Docs](https://learn.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&WT.mc_id=DT-MVP-4015686&tabs=yaml)
