持续集成之 Nuget 进阶

作者: 天天向上卡索 | 来源:发表于2019-04-07 12:38 被阅读7次

    持续集成之 Nuget 进阶

    Intro

    之前介绍了一篇基于 Azure pipeline 的 nuget 包的持续集成配置,但是比较粗糙,这里介绍一下结合 Cake 实现更优雅的 nuget 包发布流程。

    实现目标:

    1. 分支(除master/preview)有代码 push 或者 pr 时 自动 build
    2. preview 分支有代码 push 的时候将 build 并将发布 preview 版的 nuget 包
    3. master 分支有代码 push 的时候将 build 并将发布稳定版的 nuget 包

    什么是Cake?为什么要使用 Cake?

    Cake 是C# Make的缩写,是一个基于C# DSL的自动化构建系统。它可以用来编译代码,复制文件以及文件夹,运行单元测试,压缩文件以及构建Nuget包等等。

    熟悉大名鼎鼎的Make的小伙伴,应该已经知道Cake大致是个什么样的工具了,Cake具有以下几个特点:

    1. 方便编写:使用基于C#的DSL,非常易于编写自动化的脚本。
    2. 跨平台: 基于Roslyn和Mono来编译我们写的自动化脚本,使得它可以运行在windows,linux,mac上。
    3. 可靠的:可以建立在自己的机器上,也可以建立在像AppVeyor,TeamCity,TFS,VSTS或Jenkins这样的CI系统上,都可以以相同的方式运行。
    4. 丰富的工具集:支持MSBuild,MSTest,xUnit,NUnit,Nuget,ILMerge,Wix和SignTool等等,以及支持丰富的插件(Cake Addins)。
    5. 开源:基于MIT开放源代码(Cake on Github),并且是.NET 基金会支持的一个项目(Cake on dotnet foundation)。

    最初做自动化发布的时候自己尝试去写 powershell 和 bash shell 脚本,但是写的多了一点会发现,很多语法不太一致,往往写一个功能要写一个 powershell 脚本 再写一个 bash shell 脚本,徒然增加自己的工作量,而且有时候会发生一些奇怪的问题,在Windows上的路径和Linux的路径有时候会不同,使用了 Cake,我们就只需要专注于脚本要执行的过程,不需要关注 powershell 和 bashshell 的不同,不需要太多关注于 windows 和 linux 的差异。

    使用 Cake

    Cake 有 Visual Studio Code 插件,可以基于 VSCode 来编辑 cake 脚本

    Cake 脚本示例

    cake 主要文件:

    • build.ps1/build.sh 启动脚本,build.ps1 为 Windows 系统上要执行的 powershell 脚本,build.sh 为 *nix 上要执行的 shell 脚本
    • build.cake 实际执行的脚本,定义各种 build 需要的 task
    • tools/packages.config 启动脚本需要的 nuget 包

    添加 cake 支持之后,你可能需要修改 .gitignore,官方推荐的 gitignore 是这样的

    tools/**
    !tools/package.config
    

    实际使用下来,即使没有 package.config 也是可以正常工作的,可以简化为一条

    tools/**
    

    示例项目

    这里以我的一个个人开源项目 WeihanLi.Redis 为例

    cake 脚本

    ///////////////////////////////////////////////////////////////////////////////
    // ARGUMENTS
    ///////////////////////////////////////////////////////////////////////////////
    
    var target = Argument("target", "Default");
    var configuration = Argument("configuration", "Release");
    
    var solutionPath = "./WeihanLi.Redis.sln";
    var srcProjects  = GetFiles("./src/**/*.csproj");
    var testProjects  = GetFiles("./test/**/*.csproj");
    
    var artifacts = "./artifacts/packages";
    var isWindowsAgent = (EnvironmentVariable("Agent_OS") ?? "Windows_NT") == "Windows_NT";
    var branchName = EnvironmentVariable("BUILD_SOURCEBRANCHNAME") ?? "local";
    
    ///////////////////////////////////////////////////////////////////////////////
    // SETUP / TEARDOWN
    ///////////////////////////////////////////////////////////////////////////////
    
    Setup(ctx =>
    {
       // Executed BEFORE the first task.
       Information("Running tasks...");
       PrintBuildInfo();
    });
    
    Teardown(ctx =>
    {
       // Executed AFTER the last task.
       Information("Finished running tasks.");
    });
    
    ///////////////////////////////////////////////////////////////////////////////
    // TASKS
    ///////////////////////////////////////////////////////////////////////////////
    
    Task("clean")
        .Description("Clean")
        .Does(() =>
        {
           var deleteSetting = new DeleteDirectorySettings()
           {
              Force = true,
              Recursive = true
           };
          if (DirectoryExists(artifacts))
          {
             DeleteDirectory(artifacts, deleteSetting);
          }
        });
    
    Task("restore")
        .Description("Restore")
        .Does(() => 
        {
          foreach(var project in srcProjects)
          {
             DotNetCoreRestore(project.FullPath);
          }
        });
    
    Task("build")    
        .Description("Build")
        .IsDependentOn("clean")
        .IsDependentOn("restore")
        .Does(() =>
        {
          var buildSetting = new DotNetCoreBuildSettings{
             NoRestore = true,
             Configuration = configuration
          };
          foreach(var project in srcProjects)
          {
             DotNetCoreBuild(project.FullPath, buildSetting);
          }
        });
    
    Task("test")    
        .Description("Test")
        .IsDependentOn("build")
        .Does(() =>
        {
          var testSettings = new DotNetCoreTestSettings{
             NoRestore = true,
             Configuration = configuration
          };
          foreach(var project in testProjects)
          {
             DotNetCoreTest(project.FullPath, testSettings);
          }
        });
    
    
    Task("pack")
        .Description("Pack package")
        .IsDependentOn("test")
        .Does(() =>
        {
          var settings = new DotNetCorePackSettings
          {
             Configuration = configuration,
             OutputDirectory = artifacts,
             VersionSuffix = "",
             NoRestore = true,
             NoBuild = true
          };
          if(branchName != "master"){
             settings.VersionSuffix = $"preview-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
          }
          foreach (var project in srcProjects)
          {
             DotNetCorePack(project.FullPath, settings);
          }
          PublishArtifacts();
        });
    
    bool PublishArtifacts()
    {
       if(!isWindowsAgent)
       {
          return false;
       }
       if(branchName == "master" || branchName == "preview")
       {
          var pushSetting =new DotNetCoreNuGetPushSettings
          {
             Source = EnvironmentVariable("Nuget__SourceUrl") ?? "https://api.nuget.org/v3/index.json",
             ApiKey = EnvironmentVariable("Nuget__ApiKey")
          };
          var packages = GetFiles($"{artifacts}/*.nupkg");
          foreach(var package in packages)
          {
             DotNetCoreNuGetPush(package.FullPath, pushSetting);
          }
          return true;
       }
       return false;
    }
    
    void PrintBuildInfo(){
       Information($@"branch:{branchName}, agentOs={EnvironmentVariable("Agent_OS")}
       BuildID:{EnvironmentVariable("BUILD_BUILDID")},BuildNumber:{EnvironmentVariable("BUILD_BUILDNUMBER")},BuildReason:{EnvironmentVariable("BUILD_REASON")}
       ");
    }
    
    Task("Default")
        .IsDependentOn("pack");
    
    RunTarget(target);
    

    我这里使用 Azure pipeline 来实现持续集成,上面的里面有一些Azure pipeline 的变量,实际执行 build.ps1 脚本

    Azure pipeline config

    trigger:
    - '*'
    
    pool:
      vmImage: 'vs2017-win2016'
    
    steps:
    - script: dotnet --info
      displayName: 'dotnet info'
    
    - powershell: ./build.ps1
      displayName: 'Powershell Script'
      env:
        Nuget__ApiKey: $(nugetApiKey)
        Nuget__SourceUrl: $(nugetSourceUrl)
    

    nugetApiKey 是比较敏感的信息,在 Azure Pipeline 里的 Variables 的 Secret 变量,这里需要转换一下,不然,直接从环境变量读取是读取不到的,详细参考:https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch&viewFallbackFrom=vsts#secret-variables

    通过以上脚本可以本文开篇提到的目标:

    1. 分支(除master/preview)有代码 push 或者 pr 时 自动 build
    2. preview 分支有代码 push 的时候将 build 并将发布 preview 版的 nuget 包
    3. master 分支有代码 push 的时候将 build 并将发布稳定版的 nuget 包

    preview 和 master 分支可以设置 branch policy,设置只能由 pull request 合并,不能直接 push 代码,如果必须要先发布 preview 再发布稳定版 nuget 包,可以添加自定以限制,限制 master 分支的代码只能从 preview 分支通过 pr 合并

    Reference

    相关文章

      网友评论

        本文标题:持续集成之 Nuget 进阶

        本文链接:https://www.haomeiwen.com/subject/cnajiqtx.html