Nuke: Konfigurieren des Erstellens und Publizierens eines .NET-Projekts

Einführung

Derzeit werden viele CI / CD-Systeme verwendet. Jeder hat bestimmte Vor- und Nachteile, und jeder wählt die für das Projekt am besten geeignete aus. Der Zweck dieses Artikels besteht darin, Sie mit Nuke am Beispiel eines Webprojekts vertraut zu machen, das das pensionierte .NET Framework verwendet, um eine weitere Aktualisierung auf .NET 5 durchzuführen. Das Projekt verwendet bereits den Fake-Collector, musste jedoch aktualisiert und verfeinert werden, was letztendlich zum Übergang führte auf Nuke.





Ausgangsdaten

  • In C # geschriebenes Webprojekt basierend auf .NET Framework 4.8, Razor Pages + TypeScript-Frontend-Skripten, die in JS-Dateien kompiliert wurden.





  • Erstellen und veröffentlichen Sie Ihre Anwendung mit Fake 4 .





  • Hosting auf AWS (Amazon Web Services)





  • Einstellung: Produktion, Inszenierung, Demo





Tor

Es ist erforderlich, das Build-System zu aktualisieren und gleichzeitig Erweiterbarkeit und flexible Anpassung bereitzustellen. Sie müssen auch sicherstellen, dass die Konfiguration in der Datei Web.config für die angegebene Umgebung konfiguriert ist.

Ich habe verschiedene Optionen für Build-Systeme in Betracht gezogen und am Ende fiel die Wahl auf Nuke , da es recht einfach ist und tatsächlich eine Konsolenanwendung ist, die durch Pakete erweiterbar ist. Darüber hinaus ist Nuke sehr dynamisch und gut dokumentiert . Ein Plus ist das Vorhandensein eines Plugins für die IDE (Development Environment - Rider). Ich habe mich geweigert, zu Fake 5 zu wechseln, weil ich die sprachliche Konsistenz des Projekts sicherstellen und die Einstiegsschwelle für neue Entwickler senken wollte. Außerdem sind Skripte schwieriger zu debuggen. Kuchen , Psake ließ es auch wegen seiner " Skripterstellung" fallen.





Vorbereitung

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 .







Der Code kann verbessert werden, indem einige Stufen in separate Ziele unterteilt werden, wodurch die Länge des Codes in Methoden verringert wird, indem die Möglichkeit hinzugefügt wird, einzelne Stufen zu deaktivieren. Der Zweck des Artikels ist es jedoch, den Nuke-Sammler vorzustellen und die Verwendung anhand eines realen Beispiels zu zeigen.












All Articles