使用 Cake 推送 NuGet 包到 AzureDevops 的 Artifacts 上

Wigor發表於2019-06-28

前言

大家好,我最近在想如何提交程式碼的時候自動的打包 NuGet 然後釋出到 AzureDevOps 中的 Artifacts,在這個過程中踩了很多坑,也走了很多彎路,所以這次篇文章就是將我探索的結果和我遇到的一些問題整理分享給大家。

我的上一篇關於 CI/CD 的文章《使用 Gitlab CI/CD 實現自動化釋出站點到 IIS》 中是使用指令碼的形式實現的,後來有園友在下面評論說可以使用 Cake(C# Make) 這個工具來實現其中的功能,所以本次就不用了指令碼了。有時間會使用 Cake 對它進行改造。

整體思路:

  1. 首先介紹下 CakeAzureDevops Pipelines/Artifacts 怎麼使用

  2. 接著配置 AzureDevops Pipelines

  3. 建立 AzureDevops Artifacts (NuGet 服務端)

  4. AzureDevops 配置 PAT (Personal Access Tokens) 和 Pipelines 所需的 Variables(變數)

  5. Cake 增加打包、推送 NuGet 包程式碼。

  6. 最後檢視執行結果

使用到的工具及版本:

dotnet core 2.2

cake 0.33.0

PowerShell、NuGet、CredentialProvider

AzureDevops Pipelines 和 AzureDevops Artifacts

介紹

  • Cake 的全稱是 C# Make,它是一個跨平臺的自動化構建系統,基於 C# DSL,所以可以用我們熟悉的 C# 語言來替換掉我們之前使用指令碼的構建方式。使用它我們能非常方便的編譯程式碼,複製檔案和資料夾,當然還可以執行單元測試以以確保我們的程式碼沒有問題,我們本次的 NuGet 釋出到 Artifacts 它佔很重要的地位。

  • AzureDevops 的前身是 VSTS,它提供了 Repos、Pipelines、Boards、Test Plans、Artifacts:
    • Repos 提供 Git 儲存庫,用於程式碼的原始碼控制,你可以直接引入你在 GitHub 上的倉儲。
    • Pipelines 提供構建和釋出服務,以支援應用程式的持續整合和交付(CI/CD)
    • Boards 提供了一套 Agile 工具,支援使用看板和 Scrum 方法規劃和跟蹤工作,程式碼缺陷管理等等,類似的工具有騰訊的 TAPD、阿里的 雲效、華為雲的 DevCloud 等等。
    • Test Plans 提供了多種測試應用程式的工具,包括手動/探索性測試和持續測試
    • Artifacts 允許團隊從公共和私人來源共享 Maven,npm 和 NuGet 包。
      協作工具,包括可自定義的團隊儀表板和可配置的小部件,以共享資訊,進度和趨勢; 用於共享資訊的內建 wiki; 可配置的通知等。
  • CredentialProvider 是憑據提供程式,當我們進行 NuGet Push 時需要進行身份驗證,只需要將它放在 NuGet 程式的下即可。

Cake 安裝和使用

本次案例我已經發布到 GitHub 上了:https://github.com/WuMortal/CakePushNuGet.Example

安裝:這裡我使用 dotnet core 進行演示,cake 還支援 .NET Framework、Mono。首先我們需要安裝 cake,藉助 dotnet tool 這個命令。

dotnet tool install --global cake.tool --version 0.33.0

安裝成功會出現如下提示:

cake 安裝

cake 的使用方式非常簡單,而且還是 C# 語法相信應該是很容易就能理解的。

這裡首先定義了一個 target 變數,它裡面儲存的就是我們將要執行的 Task(任務)的名稱。接著可以看到在在程式碼塊中定義了許多的 Task,這裡就是具體需要執行的 “任務”,第一個任務是還原專案的依賴,其實核心程式碼就一行 DotNetCoreRestore(solution);,第二個任務是生成專案,需要說明的是第三個任務其實是將前面兩個任務整合到一起。你也可以在中第二個任務 .IsDependentOn ("Restore") 呼叫第一個任務,當然 var target = Argument ("target", "Demo"); 就需要改為 var target = Argument ("target", "Build"); 了,這個看個人喜好了。

cake 安裝

var rootPath = "../";   //根目錄
var srcPath = rootPath + "src/";    
var solution = srcPath + "Wigor.CakePushNuGet.Example.sln";   //解決方案檔案
//需要執行的目標任務
var target = Argument ("target", "Demo");

Task ("Restore")
  .Description ("還原專案依賴")
  .Does (() => {
    //Restore
    Information ("開始執行還原專案依賴任務");
    DotNetCoreRestore (solution);
  });

Task ("Build")
  .Description ("編譯專案")
  .Does (() => {
    Information ("開始執行編譯生成專案任務");
    //Build
    DotNetCoreBuild (solution, new DotNetCoreBuildSettings {
        NoRestore = true,   //不執行還原,上一步已經還原過了
        Configuration = "Release"
    });
  });

// 執行的任務
Task ("Demo")
  .IsDependentOn ("Restore")    //1. 執行上面的 Restore 任務
  .IsDependentOn ("Build")      //2. 需要執行 上面的 Build 任務
  .Does (() => {
    Information ("所有任務完成");
  });

//執行目標任務 Demo
RunTarget (target);

cake 編寫好後我們就可以嘗試執行它,這裡我的 cake 路徑是 build/build.cake 大家可以根據具體情況更改 ,命令如下:

dotnet cake build/build.cake -verbosity=diagnostic

cake 執行

可以看到這裡的 cake 已經執行成功了,它會將我們每個任務執行的結果和資訊顯示在控制檯上。

cake 執行

到這裡相信大家對 cake 是幹什麼了有點了解吧,有關它跟多的使用方法可以訪問官網:https://cakebuild.net/

AzureDevops Pipelines 使用

首先你需要一個 Microsoft 賬號或者 GitHub 賬號,登入地址為:https://dev.azure.com,登入之後你需要建立一個專案,這裡我已經建立好一個專案了,首先我們點選 Pipelines 選擇 Builds,之後會出現如下介面,點選 New Pipeline。然後跟著我下面圖片的步驟一步一步來就行。

AzureDevops Pipelines 使用

如果你的倉儲就在 AzureDevops上那麼直接選 Azure Repos Git 就行。

AzureDevops Pipelines 使用

這裡你的賬號是 GitHub 授權登入的話會先跳轉到授權介面可能會跳轉多次,同意即可。

AzureDevops Pipelines 使用
AzureDevops Pipelines 使用

刪除我選中的程式碼,因為我不打算用 AzureDevops Pipelines 的指令碼來執行本次操作,它做的只是提供我們 cake 執行的環境。

AzureDevops Pipelines 使用

更換為如下指令碼,PowerShell.exe -file ./cake.ps1 是指使用 PowerShell 執行我們的 cake.ps1 檔案,關於 cake.ps1 檔案後面會介紹,這裡我們先這樣寫,接著點選 Save and run

AzureDevops Pipelines 使用

trigger:
- master

pool:
  vmImage: 'windows-latest'

steps:
- script: PowerShell.exe -file ./cake.ps1
  displayName: 'Push NuGet Package'

可以看到問們管道的執行出現了錯誤,那是因為我們上面在執行了 cake.ps1 這個指令碼,但是我們現在還沒有建立這個指令碼。

AzureDevops Pipelines 使用

回到我們的專案中,將 AzureDevops Pipelines 建立的 azure-pipelines.yml 檔案 pull 到我們本地。

AzureDevops Pipelines 使用

接著我們編寫我們下面缺少的 cake.ps1 檔案,它做的事情就是將我們之前手動在 cmd 中執行的命令放入了一個 PowerShell 指令碼檔案中,Linux 平臺的話就編寫一個 shell 指令碼。

AzureDevops Pipelines 使用

# Install cake.tool
dotnet tool install --global cake.tool --version 0.33.0

# 輸出將要執行的命命令
Write-Host "dotnet cake build\build.cake -verbosity=diagnostic" -ForegroundColor GREEN

dotnet cake build\build.cake -verbosity=diagnostic

嘗試專案根目錄下執行這個指令碼,在 cmd 中執行 powershell .\cake.ps1,下面報了一個錯。
AzureDevops Pipelines 使用

我們只需要以管理員身份執行 PowerShell 然後執行 set-ExecutionPolicy RemoteSigned 即可

AzureDevops Pipelines 使用

AzureDevops Pipelines 使用

然後再次執行 powershell .\cake.ps1 或者命令,可以看到正確的輸出了

AzureDevops Pipelines 使用

OK,這次我們推送(git push)下程式碼,在到 AzureDevops Pipelines 看看我們執行結果。
AzureDevops Pipelines 使用

點進去可以看到整個執行的過程,如果報錯了也可以從這裡看到出錯的資訊

AzureDevops Pipelines 使用

如果是 powershell 報錯 AzureDevops Pipelines 是不會顯示執行失敗的,如果沒得到你想要的結果你就需要點開認真的分析你的指令碼了。

AzureDevops Artifacts 使用

前面已經講過了如果使用 cake 和 在 AzureDevops Pipelines 下執行 cake。下面我們需要建立一個 NuGet Repository,這裡我使用 AzureDevops 提供的 Artifacts。

AzureDevops Artifacts 使用
AzureDevops Artifacts 使用
AzureDevops Artifacts 使用

這裡面會用的就是 package source URL 和下面命令中的 -ApiKey 中的 AzureDevOps,還有這裡我們需要將 NuGet + Credentials Provider 下載到我們的本地,如果你的執行環境是 Linux 或其他可以在 microsoft/artifacts-credprovider 的 GitHub 上獲取對應平臺的這兩個包, 點選檢視 GitHub 地址

建立 PAT (Personal Access Tokens)

上面說過了我們推送 NuGet 包到 Artifacts 時候是需要為兩個引數提供指的的 -UserName 和 -Password,這裡的 UserName 我們可以隨意填,但是 Password 填的的是我們建立的 PAT。

APersonal Access Tokens 使用
Personal Access Tokens 使用

這是選擇我們 PAT 所擁有的許可權,需要點選 Show all scopes 找到 Packaging 勾選 Red,wirte,& manage

Personal Access Tokens 使用

我們可以看到我們的 PAT ,需要注意的是這個 token 只會出現一次,你需要將它儲存好,如果忘記了,那麼可以點選 Regenerate 重新獲取 token。

Personal Access Tokens 使用

AzureDevops Pipelines 新增變數

上一篇文章 中我說過了為什麼需要變數,這裡就不重複了,有興趣的可以看看。下面開始新增我們需要的變數。

新增變數 使用
A新增變數 使用

我們需要新增的變數有四個,分別是 NUGET_REPOSITORY_API_URLNUGET_REPOSITORY_API_KEYUSERNAMEPASSWORD

  • NUGET_REPOSITORY_API_URL:就是我們在建立 AzureDevops Artifacts 後出現的 package source URL

  • NUGET_REPOSITORY_API_KEY:就是那個 -ApiKey 引數的值 AzureDevOps

  • USERNAME:這個上面說過了可以隨便填。

  • PASSWORD:這個就是之前建立的 PAT。

點選儲存(Save & queue)或者 Ctrl + s 儲存。

新增變數 使用

新增 NuGet.Tool.cake 和 NuGet.exe、Credentials Provider

這裡為已經封裝過了的工具類包含了打包和推送方法,地址:NuGet.Tool.cake

using System;
using System.Collections.Generic;
using System.Linq;
using Cake.Common.Tools.DotNetCore;
using Cake.Common.Tools.DotNetCore.Pack;
using Cake.Common.Tools.NuGet;
using Cake.Common.Tools.NuGet.List;
using Cake.Core;
using NuGet.Packaging;

public class NuGetTool {
    public ICakeContext CakeContext { get; }

    public string RepositoryApiUrl { get; }

    public string RepositoryApiKey { get; }

    public string UserName { get; set; }

    public string Password { get; set; }

    private NuGetListSettings ListSettings => new NuGetListSettings {
        AllVersions = true,
        Source = new string[] { this.RepositoryApiUrl }
    };

    private DotNetCorePackSettings BuildPackSettings (string packOutputDirectory) => new DotNetCorePackSettings {
        Configuration = "Release",
        OutputDirectory = packOutputDirectory,
        IncludeSource = true,
        IncludeSymbols = true,
        NoBuild = false
    };

    private NuGetTool (ICakeContext cakeContext) {
        CakeContext = cakeContext;
        RepositoryApiUrl = cakeContext.Environment.GetEnvironmentVariable ("NUGET_REPOSITORY_API_URL");
        RepositoryApiKey = cakeContext.Environment.GetEnvironmentVariable ("NUGET_REPOSITORY_API_KEY");
        UserName = cakeContext.Environment.GetEnvironmentVariable ("USERNAME");
        Password = cakeContext.Environment.GetEnvironmentVariable ("PASSWORD");
        CakeContext.Information ($"獲取所需引數成功:{RepositoryApiUrl}");
    }

    public static NuGetTool FromCakeContext (ICakeContext cakeContext) {
        return new NuGetTool (cakeContext);
    }

    public void Pack (List<string> projectFilePaths, string packOutputDirectory) {
        projectFilePaths.ForEach (_ => CakeContext.DotNetCorePack (_, BuildPackSettings (packOutputDirectory)));
    }

    public void Push (List<string> packageFilePaths) {

        foreach (var packageFilePath in packageFilePaths) {
            CakeContext.NuGetAddSource (
                "wigor",
                this.RepositoryApiUrl,
                new NuGetSourcesSettings {
                    UserName = this.UserName,
                    Password = this.Password
                });

            CakeContext.NuGetPush (packageFilePath, new NuGetPushSettings {
                Source = "wigor",
                ApiKey = this.RepositoryApiKey
            });

        }

    }
}

在專案的 build/ 下建立 nuget.tool.cake 檔案(build/nuget.tool.cake) 拷貝上面的程式碼。

這裡參考了最開始提到的園友的專案,非常感謝它的貢獻,GitHub 地址如下:cake.example

在建立 AzureDevops Artifacts 的時候那不是提供了 NuGet + Credentials Provider 的下載地址嘛,現在把它解壓到我們專案的 build\tool\ 下。再次說明這裡我是 Windows 環境,如果你的執行環境是 Linux 或其他可以在 microsoft/artifacts-credprovider 的 GitHub 上獲取對應平臺的這兩個包, 點選檢視 GitHub 地址

NuGet.exe、Credentials Provider

修改 cake.ps1 和 build.cake 檔案

修改 cake.ps1,只是增加了 NuGet.exe 的環境變數,因為不加到時候 cake 會找不到 NuGet.exe,或許還有其他辦法這裡就先這麼幹,如果各位還有更方便的方法可以在下面留言,感謝!

# 執行的檔案
[string]$SCRIPT = 'build/build.cake'

[string]$CAKE_VERSION = '0.33.0'

# 配置 NuGet 環境變數
$NUGET_EXE = "build/tool/NuGet.exe"
$NUGET_DIRECTORY = Get-ChildItem -Path $NUGET_EXE
$NUGET_DIRECTORY_NAME=$NUGET_DIRECTORY.DirectoryName
$ENV:Path += ";$NUGET_DIRECTORY_NAME"

# Install cake.tool
dotnet tool install --global cake.tool --version $CAKE_VERSION

# 引數:顯需要執行cake 執行資訊
[string]$CAKE_ARGS = "-verbosity=diagnostic"

# 輸出將要執行的命命令
Write-Host "dotnet cake $SCRIPT $CAKE_ARGS $ARGS" -ForegroundColor GREEN

dotnet cake $SCRIPT $CAKE_ARGS $ARGS

修改 build.cake 檔案,看著是多了很多東西其實就多了兩個 Task (任務) 分別是: pack(打包)push(推送包),這裡需要大家需要修改的就是 solutionproject 兩個變數,將其修改為自己的解決方案名稱和需要打包的專案名稱。

#reference "NuGet.Packaging"

#load nuget.tool.cake

var target = Argument ("target", "PushPack");

var rootPath = "../";
var srcPath = rootPath + "src/";
var solution = srcPath + "Wigor.CakePushNuGet.Example.sln";
var project = GetFiles (srcPath + "Wigor.CakePushNuGet.HelloWorld/*.csproj");
var nugetPakcageDirectory = $"{srcPath}nugetPackage/";

var nugetTool = NuGetTool.FromCakeContext (Context);

Task ("Restore")
  .Description ("還原專案依賴")
  .Does (() => {
    //Restore
    Information ("開始執行還原專案依賴任務");
    DotNetCoreRestore (solution);
  });

Task ("Build")
  .Description ("編譯專案")
  .Does (() => {
    Information ("開始執行編譯生成專案任務");
    //Build
    DotNetCoreBuild (solution, new DotNetCoreBuildSettings {
        NoRestore = true,
        Configuration = "Release"
    });
  });

Task ("UnitTest")
  .Description ("單元測試")
  .Does (() => {
    Information ("開始執行單元測試任務");
    
    DotNetCoreTest(solution);
  });

Task ("Pack")
  .Description ("Nuget 打包")
  .Does (() => {
    Information ("開始執行打包任務");

    // 確保目錄存在
    EnsureDirectoryExists (nugetPakcageDirectory);

    var packageFilePaths = project.Select (_ => _.FullPath).ToList ();

    nugetTool.Pack (packageFilePaths, nugetPakcageDirectory);
  });

Task ("Push")
  .Description ("Nuget 釋出")
  .Does (() => {
    Information ("開始執行 Nuget 包釋出任務");
    var packageFilePaths = GetFiles ($"{nugetPakcageDirectory}*.symbols.nupkg").Select (_ => _.FullPath).ToList ();

    nugetTool.Push(packageFilePaths);
  });

Task ("PushPack")
  .Description ("釋出 Nuget 包")
  .IsDependentOn ("Restore")
  .IsDependentOn ("Build")
  .IsDependentOn ("Pack")
  .IsDependentOn ("Push")
  .Does (() => {
    Information ("所有任務完成");
  });

RunTarget (target);

最後我們推送修改後的程式碼,檢視執行結果看看 NuGet 包是否釋出到 AzureDevops Artifacts 上。

結果

至此已經實現了 使用 Cake 推送 NuGet 包到 AzureDevops 的 Artifacts 上,你如果不熟悉 AzureDevops Pipelines 你也可以用其他的 CI/CD 工具來執行。

補充

在整個嘗試過程中肯定會出現一些問題,不要著急認真分析,看看 AzureDevops Pipelines 上給出的提示,也可以現在本機跑一下看看是否正常。出現問題第一步檢視錯誤資訊,看看有沒有錯誤資訊(基本都有),然後根據錯誤資訊去分析是我們的那個地方出錯了,順序是 cake.ps1 --> build.cake --> nuget.tool.cake,然後是所需的 PAT 的許可權是否勾選,AzureDevops Pipelines 變數是否配置並且是 URL、Key 什麼的都是正確,再然後就是 百度、Google。最後你可以在評論區留言(分享你碰到的問題以及解決方法)。

相關文獻

在這裡感謝各位的貢獻!

《[Cake] 0. C#Make自動化構建-簡介》

《2. dotnet 全域性工具 cake》

《基於cake-build的dotnet自動化釋出》

《Pushing Packages From Azure Pipelines To Azure Artifacts Using Cake》

AzureDevops Pipelines 變數相關文件

《認識一下 Azure DevOps》
參考專案:CakePushNuGet.Example

參考專案:cake.example

相關文章