pengantar
Ada banyak sistem CI / CD saat ini. Setiap orang memiliki kelebihan dan kekurangan tertentu, dan setiap orang memilih yang paling cocok untuk proyek tersebut. Tujuan artikel ini adalah untuk mengenalkan Anda dengan Nuke menggunakan contoh proyek web menggunakan .NET Framework yang dihentikan dengan maksud untuk memperbarui lebih lanjut ke .NET 5. Proyek tersebut sudah menggunakan pengumpul palsu, tetapi ada kebutuhan untuk memperbarui dan menyempurnakannya, yang pada akhirnya mengarah pada transisi tentang Nuke.
Data awal
Sebuah proyek web yang ditulis dalam C # berdasarkan pada .NET Framework 4.8, skrip frontend Razor Pages + TypeScript yang dikompilasi ke file JS.
Bangun dan publikasikan aplikasi Anda menggunakan Fake 4 .
Hosting di AWS (Amazon Web Services)
Pengaturan: Produksi, Pementasan, Demo
tujuan
Sistem build perlu diperbarui, sambil memberikan ekstensibilitas dan kustomisasi yang fleksibel. Anda juga perlu memastikan bahwa konfigurasi di file Web.config dikonfigurasi untuk lingkungan yang ditentukan.
Saya mempertimbangkan opsi yang berbeda untuk membangun sistem dan pada akhirnya pilihan jatuh pada Nuke , karena cukup sederhana dan sebenarnya adalah aplikasi konsol yang dapat dikembangkan dengan paket. Selain itu, Nuke cukup dinamis dan terdokumentasi dengan baik . Nilai tambahnya adalah adanya plugin untuk IDE (lingkungan pengembangan - Rider). Saya menolak untuk beralih ke Fake 5 karena keinginan untuk memastikan konsistensi linguistik proyek dan untuk menurunkan ambang batas masuk untuk pengembang baru. Selain itu, skrip lebih sulit untuk di-debug. Kue , Psake juga menjatuhkannya karena "scripting" -nya.
Persiapan
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 .
Kode dapat ditingkatkan dengan memecah beberapa tahapan menjadi Target terpisah, mengurangi panjang kode dalam metode dengan menambahkan kemampuan untuk menonaktifkan tahapan individu. Tetapi tujuan artikel ini adalah untuk memperkenalkan pengumpul Nuke dan menunjukkan penggunaannya dengan contoh nyata.