開發 .NET 專案時,編譯跟部署時常伴隨一些相關作業。

我最近遇到的例子是修改 Telegram AI 機器人,修改測試 OK 後,下個動作是編譯 Docker Image,存成 tar 檔,scp 上傳到家裡的 Linux 主機,執行 docker load 更新再重啟容器。

這類作業的最高境界是用 Github 或其他版控伺服器實現 CI/CD。當 Git 觸發 Push 或 PR 時,版控伺服器負責編譯 .NET 專案、打包成 Docker Image,並推送到 Container Registry。目的主機則執行 Watchtower 容器,定時檢查 Docker Image 是否有新版本,若有就自動拉取並重啟容器。

CI/CD 是吾人該追求的完全體,而我目前的解法很拙劣 - 寫個 .bat 檔,在裡面跑 dotnet publish、docker build、docker save、scp 複製、ssh 遠端跑指令 docker load 及 docker compose up -d。現階段我打算先前進一步,找到比手刻 .bat 優雅一點的做法,累積一些基本功,未來再持續進化。

評估後,在 .csproj 裡定義 <Target> 自訂任務是個不錯解法,將編譯、部署相關邏輯包進專案定義,比起另外帶一個 .bat/.ps1 拖油瓶優雅多了。

分享手邊兩個應用實例。

案例一,AI 模型成本報表網站,原本靠筆記記指令跟寫 .bat 把一串動作包在一起。改良版變成在 .csproj 在新增 BuildDocker 與 DeployDocker 兩個 Task 後,用 dotent msbuild -t:BuildDocker 建置容器 Image,用 dotnet msbuild -t:DeployDocker 完成 Image 轉存 tar、scp 上傳 Linux、透過 ssh 在 Linux 上完成更新 Image 及重啟 Docker。(註:要先設好 金鑰免密碼登入 SSH/SCP。延伸閱讀:免工具快速部署 SSH 金鑰到遠端 Linux 主機、WSL 與 Windows 共用 SSH 金鑰登入遠端主機)

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <Target Name="BuildDocker">
    <Exec Command="docker build -t aoai-cost-report:latest ." />
  </Target>

  <Target Name="DeployDocker">
    <Exec Command="docker save aoai-cost-report:latest -o aoai-cost-report.tar" />  
    <Exec Command="scp aoai-cost-report.tar my-home-server:~/dockers/aoai-cost-report" />
    <Exec Command="ssh my-home-server 'cd ~/dockers/aoai-cost-report &amp;&amp; docker load -i aoai-cost-report.tar &amp;&amp; docker compose up -d'" />
  </Target>

</Project>

案例二,自己寫的桌面小工具,編譯的 .exe 檔要複製到 X:\Tools\Homebrew\Sidi 目錄下,我在 .csproj 加了一個 Target,指定每次 Publish 後該目錄下的 .exe 更新為新版本,另外為防止程式還在執行,檔案被鎖定無法覆寫,先使用 Taskkill 試著將已經在執行中的程序刪除。如此,每次下 dotnet publish 時便會自動更新。
有個小眉角是需要加上 dotnet publish -tl:off 關掉 .NET SDK 的 Terminal Logger,用比較精簡美觀的格式顯示輸出,但會過濾掉 MSBuild 的 任務輸出; 加上 --tl:off 後會改用傳統的 MSBuild console logger,所有訊息(包含 <Message Importance="High">)才會原樣顯示。另個替代做法是改寫成 <Exec Command="echo ...">,如此不需顧慮 Terminal Logger,也是不錯選擇。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <PublishSingleFile>true</PublishSingleFile>
  </PropertyGroup>

  <Target Name="DeployFiles" AfterTargets="Publish">
    <Exec Command="taskkill /IM Sidi.exe /F 2&gt;nul" IgnoreExitCode="true" />
    <Copy SourceFiles="$(PublishDir)Sidi.exe" DestinationFolder="X:\Toos\Homebrew\Sidi" />
    <Message Text="X:\Toos\Homebrew\Sidi\Sidi.exe is updated" Importance="High" />
  </Target>

</Project>

Explains using custom MSBuild Target tasks in .NET projects to automate build and deployment workflows. Shows examples for Docker image build/deploy and post-publish file updates, offering a cleaner alternative to batch scripts and a step toward future CI/CD.


Comments

# by yoyo

我認為CI/CD pipeline script還是不要寫在專案檔內比較好, 另外寫在 .bat/.ps1 可降低耦合性,pipeline有問題時也更容易debug測試,再用CI/CD工具串接 gitea 也有CI/CD 功能

Post a comment