introduction
De nombreux systèmes CI / CD sont actuellement utilisés. Chacun a certains avantages et inconvénients, et chacun choisit le plus adapté au projet. Le but de cet article est de vous familiariser avec Nuke à l'aide de l'exemple d'un projet Web utilisant le .NET Framework retiré en vue de poursuivre la mise à jour vers .NET 5. Le projet utilise déjà le collecteur Fake, mais il était nécessaire de le mettre à jour et de l'affiner, ce qui a finalement conduit à la transition sur Nuke.
Donnée initiale
Un projet Web écrit en C # basé sur .NET Framework 4.8, Razor Pages + scripts TypeScript frontend compilés en fichiers JS.
Créez et publiez votre application à l'aide de Fake 4 .
Hébergement sur AWS (Amazon Web Services)
Cadre: Production, Mise en scène, Démo
objectif
Il est nécessaire de mettre à jour le système de construction, tout en offrant une évolutivité et une personnalisation flexible. Vous devez également vous assurer que la configuration dans le fichier Web.config est configurée pour l'environnement spécifié.
J'ai envisagé différentes options pour les systèmes de construction et au final, le choix s'est porté sur Nuke , car il est assez simple et en fait est une application console extensible via des packages. De plus, Nuke est assez dynamique et bien documenté . Un plus est la présence d'un plugin pour l'IDE (environnement de développement - Rider). J'ai refusé de passer à Fake 5 en raison de la volonté d'assurer la cohérence linguistique du projet et d'abaisser le seuil d'entrée pour les nouveaux développeurs. De plus, les scripts sont plus difficiles à déboguer. Cake , Psake l'a également abandonné en raison de son "scripting".
Préparation
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]. .
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 .
Le code peut être amélioré en divisant certaines étapes en cibles distinctes, en réduisant la longueur du code dans les méthodes en ajoutant la possibilité de désactiver des étapes individuelles. Mais le but de l'article est de présenter le collecteur Nuke et de montrer son utilisation avec un exemple réel.