Nuke: Configuración de construir y publicar un proyecto .NET

Introducción

Actualmente se utilizan muchos sistemas CI / CD. Todos tienen ciertas ventajas y desventajas, y todos eligen el más adecuado para el proyecto. El propósito de este artículo es familiarizarlo con Nuke usando el ejemplo de un proyecto web que usa el .NET Framework que se retira con miras a una mayor actualización a .NET 5. El proyecto ya usa el recopilador de falsificaciones, pero era necesario actualizarlo y refinarlo, lo que finalmente llevó a la transición. en Nuke.





Datos iniciales

  • Proyecto web escrito en C # basado en .NET Framework 4.8, Scripts frontend de Razor Pages + TypeScript compilados en archivos JS.





  • Cree y publique su aplicación con Fake 4 .





  • Alojamiento en AWS (Amazon Web Services)





  • Marco: Producción, Puesta en escena, Demo





objetivo

Es necesario actualizar el sistema de compilación, al tiempo que proporciona extensibilidad y personalización flexible. También debe asegurarse de que la configuración en el archivo Web.config esté configurada para el entorno especificado.

Consideré varias opciones para construir sistemas y al final la elección recayó en Nuke , ya que es bastante simple y de hecho es una aplicación de consola extensible a través de paquetes. Además, Nuke es bastante dinámico y está bien documentado . Una ventaja es la presencia de un complemento para el IDE (entorno de desarrollo - Rider). Me negué a cambiar a Fake 5 por el deseo de garantizar la coherencia lingüística del proyecto y reducir el umbral de entrada para nuevos desarrolladores. Además, los scripts son más difíciles de depurar. Cake , Psake también lo dejó debido a su "scripting".





Preparación

Nuke dotnet tool, build-. .





$ dotnet tool install Nuke.GlobalTool --global
      
      



nuke :setup



, wizard , , .





_build





boot shell- .

Build . - Target-. Logger. :





Logger.Info($"Starting build for {ApplicationForBuild} using {BuildEnvironment} environment");
      
      





. Build [Parameter]. .









  1. Nuget-













,





[Parameter("Configuration to build - Default is 'Release'")]
readonly Configuration Configuration = Configuration.Release;

[Parameter(Name="application")]
readonly string ApplicationForBuild;

[Parameter(Name="environment")]
public readonly string BuildEnvironment;
      
      



. OnBuildInitialized, , , . NukeBuild On, (, / ).





protected override void OnBuildInitialized()
{
  ConfigurationProvider = new ConfigurationProvider(ApplicationForBuild, BuildEnvironment, RootDirectory);
  string configFilePath = $"./appsettings.json";
  if (!File.Exists(configFilePath))
  {
  throw new FileNotFoundException($"Configuration file {configFilePath} is not found");
  }

  string configFileContent = File.ReadAllText(configFilePath);

  if (string.IsNullOrEmpty(configFileContent))
  {
  throw new ArgumentNullException($"Config file {configFilePath} content is empty");
  }

  /*   typescript */
  ToolsConfiguration = JsonConvert.DeserializeObject<ToolsConfiguration>(configFileContent);

  if (ToolsConfiguration == null || string.IsNullOrEmpty(ToolsConfiguration.TypeScriptCompilerFolder))
  {
  throw new ArgumentNullException($"Typescript compiler path is not defined");
  }

  base.OnBuildInitialized();
}
      
      







public class ApplicationConfig
{
  public string ApplicationName { get; set; }
  public string DeploymentGroup { get; set; }

  /*      Web.config */
  public Dictionary<string, string> WebConfigReplacingParams { get; set; }

  public ApplicationPathsConfig Paths { get; set; }
}
      
      







public class ConfigurationProvider
{
  readonly string Name;
  readonly string DeployEnvironment;
  readonly AbsolutePath RootDirectory;
  ApplicationConfig CurrentConfig;

  public ConfigurationProvider(string name, 
                               string deployEnvironment, 
                               AbsolutePath rootDirectory)
  {
    RootDirectory = rootDirectory;
    DeployEnvironment = deployEnvironment;
    Name = name;
  }

  public ApplicationConfig GetConfigForApplication()
  {
    if (CurrentConfig != null) return CurrentConfig;

    string configFilePath = $"./BuildConfigs/{Name}/{DeployEnvironment}.json";
    if (!File.Exists(configFilePath))
    {
    throw new FileNotFoundException($"Configuration file {configFilePath} is not found");
    }

    string configFileContent = File.ReadAllText(configFilePath);

    if (string.IsNullOrEmpty(configFileContent))
    {
    throw new ArgumentNullException($"Config file {configFilePath} content is empty");
    }

    CurrentConfig = JsonConvert.DeserializeObject<ApplicationConfig>(configFileContent);
    CurrentConfig.Paths = new ApplicationPathsConfig(RootDirectory, Name, CurrentConfig.ApplicationName);

    return CurrentConfig;
  }
}
      
      







Nuget-

(Clean) , . : , , (RootDirectory) :





Target Restore => _ => _
	    .DependsOn(Clean)
	    .Executes(() =>
	    {
		    NuGetTasks.NuGetRestore(config =>
		    {
			    config = config
				    .SetProcessToolPath(RootDirectory / ".nuget" / "NuGet.exe")
				    .SetConfigFile(RootDirectory / ".nuget" / "NuGet.config")
				    .SetProcessWorkingDirectory(RootDirectory)
				    .SetOutputDirectory(RootDirectory / "packages");

			    return config;
		    });
	    });
      
      







. .NET-, TypeScript- JavaScript-.





Target Compile => _ => _
  .DependsOn(Restore)
  .Executes(() =>
  {
  	AbsolutePath projectFile = ApplicationConfig.Paths.ProjectDirectory.GlobFiles("*.csproj").FirstOrDefault();

    if (projectFile == null)
    {
    	throw new ArgumentNullException($"Cannot found any projects in {ApplicationConfig.Paths.ProjectDirectory}");
    }

    MSBuild(config =>
    {
      config = config
      .SetOutDir(ApplicationConfig.Paths.BinDirectory)
      .SetConfiguration(Configuration) //  : Debug/Release
      .SetProperty("WebProjectOutputDir", ApplicationConfig.Paths.ApplicationOutputDirectory)
      .SetProjectFile(projectFile)
      .DisableRestore(); //       ,      

      return config;
    });
    /*  tsc   .       */
    IProcess typeScriptProcess = ProcessTasks.StartProcess(@"node",$@"tsc -p {ApplicationConfig.Paths.ProjectDirectory}", ToolsConfiguration.TypeScriptCompilerFolder);
    if (!typeScriptProcess.WaitForExit())
    {
    	Logger.Error("Typescript build is failed");
    	throw new Exception("Typescript build is failed");
    }

  	CopyDirectoryRecursively(ApplicationConfig.Paths.TypeScriptsSourceDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory, DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
  });
      
      







: .





Web.config . . json- .





CodeDeploy . AWS NuGet- AWSSDK: AWSSDK.Core, AWSSDK.S3, AWSSDK.CodeDeploy. AWS CodeDeploy. Build.





Target Publish => _ => _
	  .DependsOn(Compile)
		.Executes(async () =>
		    {
			    PrepareApplicationForPublishing();
          await PublishApplicationToAws();
		    });
void PrepareWebConfig(Dictionary<string, string> replaceParams)
{
  if (replaceParams?.Any() != true) return;

  Logger.Info($"Setup Web.config for environment {BuildEnvironment}");

  AbsolutePath webConfigPath = ApplicationConfig.Paths.ApplicationOutputDirectory / "Web.config";
  if (!FileExists(webConfigPath))
  {
  	Logger.Error($"{webConfigPath} is not found");
  	throw new FileNotFoundException($"{webConfigPath} is not found");
  }

  XmlDocument webConfig = new XmlDocument();
  webConfig.Load(webConfigPath);
  XmlNode settings = webConfig.SelectSingleNode("configuration/appSettings");

  if (settings == null)
  {
  	Logger.Error("Node configuration/appSettings in the config is not found");
  	throw new ArgumentNullException(nameof(settings),"Node configuration/appSettings in the config is not found");
  }

  foreach (var newParam in replaceParams)
  {
  	XmlNode nodeForChange = settings.SelectSingleNode($"add[@key='{newParam.Key}']");

  	((XmlElement) nodeForChange)?.SetAttribute("value", newParam.Value);
  }

  webConfig.Save(webConfigPath);
}

void PrepareApplicationForPublishing()
{
	AbsolutePath specFilePath = ApplicationConfig.Paths.PublishDirectory / AppSpecFile;
	AbsolutePath specFileTemplate = ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile;

	PrepareWebConfig(ApplicationConfig.WebConfigReplacingParams);

	DeleteFile(ApplicationConfig.Paths.ApplicationOutputDirectory);
	CopyDirectoryRecursively(ApplicationConfig.Paths.ApplicationOutputDirectory, ApplicationConfig.Paths.PublishDirectory / DeployAppDirectory,
	DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
	CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.TypeScriptsOutDirectory,
	DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);
	CopyFile(ApplicationConfig.Paths.BuildToolsDirectory / AppSpecTemplateFile, ApplicationConfig.Paths.PublishDirectory / AppSpecFile, FileExistsPolicy.Overwrite);
	CopyDirectoryRecursively(ApplicationConfig.Paths.BuildToolsDirectory / DeployScriptsDirectory, ApplicationConfig.Paths.PublishDirectory / DeployScriptsDirectory,
	DirectoryExistsPolicy.Merge, FileExistsPolicy.Overwrite);

	Logger.Info($"Creating archive '{ApplicationConfig.Paths.ArchiveFilePath}'");
	CompressionTasks.CompressZip(ApplicationConfig.Paths.PublishDirectory, ApplicationConfig.Paths.ArchiveFilePath);
}

async Task PublishApplicationToAws()
{
  string s3bucketName = "";
  IAwsCredentialsProvider awsCredentialsProvider = new AwsCredentialsProvider(null, null, "");
  using S3FileManager fileManager = new S3FileManager(awsCredentialsProvider, RegionEndpoint.EUWest1);
  using CodeDeployManager codeDeployManager = new CodeDeployManager(awsCredentialsProvider, RegionEndpoint.EUWest1);

  Logger.Info($"AWS S3: upload artifacts to '{s3bucketName}'");
  FileMetadata metadata = await fileManager.UploadZipFileToBucket(ApplicationConfig.Paths.ArchiveFilePath, s3bucketName);

  Logger.Info(
  $"AWS CodeDeploy: create deploy for '{ApplicationConfig.ApplicationName}' in group '{ApplicationConfig.DeploymentGroup}' with config '{DeploymentConfig}'");
  CodeDeployResult deployResult =
  await codeDeployManager.CreateDeployForRevision(ApplicationConfig.ApplicationName, metadata, ApplicationConfig.DeploymentGroup, DeploymentConfig);

  StringBuilder resultBuilder = new StringBuilder(deployResult.Success ? "started successfully\n" : "not started\n");
  resultBuilder = ProcessDeloymentResult(deployResult, resultBuilder);

  Logger.Info($"AWS CodeDeploy: deployment has been {resultBuilder}");

  DeleteFile(ApplicationConfig.Paths.ArchiveFilePath);
  Directory.Delete(ApplicationConfig.Paths.ApplicationOutputDirectory, true);
  string deploymentId = deployResult.DeploymentId;
  DateTime startTime = DateTime.UtcNow;
  /*        */
  do
  {
  	if(DateTime.UtcNow - startTime > TimeSpan.FromMinutes(30)) break;
  	Thread.Sleep(3000);
  	deployResult = await codeDeployManager.GetDeploy(deploymentId);
  	Logger.Info($"Deployment proceed: {deployResult.DeploymentInfo.Status}");
  }
  while (deployResult.DeploymentInfo.Status == DeploymentStatus.InProgress
  			|| deployResult?.DeploymentInfo.Status == DeploymentStatus.Created
  			|| deployResult?.DeploymentInfo.Status == DeploymentStatus.Queued);
  Logger.Info($"AWS CodeDeploy: deployment has been done");
}
      
      







, . , . . build .







El código se puede mejorar dividiendo algunas etapas en objetivos separados, reduciendo la longitud del código en los métodos agregando la capacidad de deshabilitar etapas individuales. Pero el propósito del artículo es presentar el colector Nuke y mostrar el uso con un ejemplo real.












All Articles