CI/CD
說下類庫的發佈流程,而不是產品發佈的流程
修改代碼->修改csproj中的版本->執行打包命令->執行上傳命令->上傳修改的文件到git倉庫
(我使用的git倉庫是碼雲,以下代碼示例都是與碼雲做的對接,使用其他倉庫也是一樣的操作流程,不同的方式)
其中修改版本----->上傳nuget包 都可以做到自動化,也就是持續交付(持續部署)
組內開發者只需要關注修改代碼->上傳代碼,等幾十秒後就會有一個新版本在nuget倉庫中出現,不再需要做重複性的工作,解放雙手
下面進入正題:
配置碼雲的webHook
它應該叫Warehouse Event 而不是WebHook😂
這就是我們的核心類
IDelivery 任務處理
IDeliveryTaskQueue 任務處理隊列
IGitProcess Git處理命令封裝
INugetProcess Nuget處理命令封裝
先看基礎的
public interface IGitProcess { /// <summary> /// 獲取Git倉庫 /// </summary> /// <param name="gitAddress"></param> /// <returns></returns> string Obtain(string gitAddress); /// <summary> /// 拉取 git 倉庫 /// </summary> /// <param name="gitAddress"></param> /// <returns></returns> string Pull(string gitAddress); /// <summary> /// 克隆Git倉庫 /// </summary> /// <param name="gitAddress"></param> string Clone(string gitAddress); /// <summary> /// 獲取倉庫名稱 /// </summary> /// <param name="gitAddress"></param> string GetGitFolderMame(string gitAddress); /// <summary> /// 提交到原創倉庫 /// </summary> /// <param name="gitAddress"></param> /// <returns></returns> string Push(string gitAddress); }
public interface INugetProcess { /// <summary> /// 打包上傳 /// </summary> /// <param name="folderPath">文件路徑</param> /// <param name="apiKey">APIKey</param> /// <param name="source">目標倉庫</param> /// <param name="author">作者</param> /// <param name="describe">描述</param> string GenerationUpload(string folderPath, string apiKey, string source, string author, string describe); /// <summary> /// 修改CsProj文件 /// </summary> /// <param name="folderPath"></param> /// <param name="author"></param> /// <param name="describe"></param> /// <returns>修改的版本</returns> string EditCsProjXml(string folderPath, string author, string describe); }
這兩個是對Git 和Nuget命令的封裝,只封裝我們需要的,不要考慮擴展性什麼的,需要什麼就寫什麼。
class RedirectRun { /// <summary> /// 功能:重定向執行 /// </summary> /// <param name="p"></param> /// <param name="exe"></param> /// <param name="arg"></param> /// <param name="output"></param> public static void RedirectExcuteProcess(Process p,string exe, string arg, DataReceivedEventHandler output, StringBuilder writeContent=null) { p.StartInfo.FileName = exe; p.StartInfo.Arguments = arg; writeContent.AppendLine(exe +" "+ arg); p.StartInfo.UseShellExecute = false; //輸出信息重定向 p.StartInfo.CreateNoWindow = true; p.StartInfo.RedirectStandardError = true; p.StartInfo.RedirectStandardOutput = true; p.OutputDataReceived += output; p.ErrorDataReceived += output; p.Start(); //啓動線程 p.BeginOutputReadLine(); p.BeginErrorReadLine(); p.WaitForExit(); //等待進程結束 } }
這個也是非常基礎的類,執行編寫的命令
/// <summary> /// Git服務 /// </summary> public class GitProcess: IGitProcess { public string Clone(string gitAddress) { StringBuilder message = new StringBuilder(); var process = new System.Diagnostics.Process(); process.StartInfo.FileName = System.Environment.CurrentDirectory + "\\" + GetGitFolderMame(gitAddress); ; RedirectRun.RedirectExcuteProcess(process, "git.exe", $"clone {gitAddress}", (x, c) => { message.AppendLine(c.Data); }, message); return message.ToString(); } public string GetGitFolderMame(string gitAddress) { var gitFolderName = gitAddress.Split("/"); return gitFolderName[gitFolderName.Length-1].Replace(".git", ""); } public string Obtain(string gitAddress) { string result = string.Empty; var gitFolderName = GetGitFolderMame(gitAddress); if (System.IO.Directory.Exists(System.Environment.CurrentDirectory + "\\" + gitFolderName)) { result = Pull(gitAddress); } else { result = Clone(gitAddress); } return result; } public string Pull(string gitAddress) { var gitFolderName = System.Environment.CurrentDirectory + "\\" + GetGitFolderMame(gitAddress); StringBuilder message = new StringBuilder(); var process = new System.Diagnostics.Process(); process.StartInfo.WorkingDirectory = gitFolderName; RedirectRun.RedirectExcuteProcess(process, "git.exe", @"pull --allow-unrelated-histories", (x, c) => { //if (csFiles!=null&&c.Data!=null&&c.Data.Contains(".cs")) //{ // csFiles.Add(gitFolderName+"\\"+c.Data.Split(" | ")[0].Replace(" ","")); //} message.AppendLine(c.Data); },message); return message.ToString(); } public string Push(string gitAddress) { var gitFolderName = System.Environment.CurrentDirectory + "\\" + GetGitFolderMame(gitAddress); StringBuilder message = new StringBuilder(); var process = new System.Diagnostics.Process(); process.StartInfo.WorkingDirectory = gitFolderName; RedirectRun.RedirectExcuteProcess(process, "git.exe", @"add .", (x, c) => { message.AppendLine(c.Data); }, message); process = new System.Diagnostics.Process(); process.StartInfo.WorkingDirectory = gitFolderName; RedirectRun.RedirectExcuteProcess(process, "git.exe", $@"commit -m '___Nuget打包任務___'", (x, c) => { message.AppendLine(c.Data); }, message); process = new System.Diagnostics.Process(); process.StartInfo.WorkingDirectory = gitFolderName; RedirectRun.RedirectExcuteProcess(process, "git.exe", @"push", (x, c) => { message.AppendLine(c.Data); }, message); return message.ToString(); return message.ToString(); } }
Obtain是一個綜合函數,由它來覺得是clone還是pull
Push 的commit -m 參數可以自定義,因爲web入口需要區分是開發者提交還是自動交付的提交
非常簡單
public class NugetProcess : INugetProcess { private FileInfo[] GetCsprojFiles(DirectoryInfo folder) { List<FileInfo> files = new List<FileInfo>(); foreach (var item in folder.GetDirectories()) { files.AddRange(GetCsprojFiles(item)); } files.AddRange(folder.GetFiles("*.csproj")); return files.ToArray(); } public string GenerationUpload(string folderPath, string apiKey, string source, string author, string describe) { StringBuilder writeContent = new StringBuilder(); FileInfo fileInfo = new FileInfo(folderPath); if (fileInfo.Extension.ToLower()!= ".csproj") { return "所選文件不爲Csproj項目文件"; } var version = EditCsProjXml(folderPath, author, describe); RedirectRun.RedirectExcuteProcess(new System.Diagnostics.Process(), "dotnet", $"build {fileInfo.FullName}", (x, c) => { writeContent.AppendLine(c.Data); }, writeContent); RedirectRun.RedirectExcuteProcess(new System.Diagnostics.Process(), "dotnet.exe", $"pack {fileInfo.FullName} --output {fileInfo.Directory.FullName}", (x, c) => { writeContent.AppendLine(c.Data); }, writeContent); RedirectRun.RedirectExcuteProcess(new System.Diagnostics.Process(), "dotnet.exe", $"nuget push {fileInfo.FullName.Replace(".csproj", $".{version}.nupkg")} --api-key {apiKey} --source {source}", (x, c) => { writeContent.AppendLine(c.Data); }, writeContent); return writeContent.ToString(); } public string EditCsProjXml(string folderPath,string author, string describe) { XmlDocument xml = new XmlDocument(); xml.Load(folderPath); XmlNode projectNode = xml.SelectSingleNode("Project"); XmlNode propertyGroupNode = projectNode.SelectSingleNode("PropertyGroup"); var result = EditVersionNumber(propertyGroupNode); EditAuthor(propertyGroupNode, author); EditDescribe(propertyGroupNode, describe); xml.Save(folderPath); return result; } private string EditVersionNumber(XmlNode node) { var versionNode = node.SelectSingleNode("Version"); if (versionNode == null) { versionNode = node.AppendChild(node.OwnerDocument.CreateElement("Version")); versionNode.InnerText = "1.0.0"; } var wholeVersion = versionNode.InnerText.Split("."); var version_int = int.Parse(wholeVersion[wholeVersion.Length - 1]) + 1; var resultVersion = new StringBuilder(); for (int i = 0; i < wholeVersion.Length - 1; i++) { resultVersion.Append(wholeVersion[i] + "."); } resultVersion.Append(version_int); versionNode.InnerText = resultVersion.ToString(); return resultVersion.ToString(); } /// <summary> /// 修改作者 /// </summary> /// <param name="content"></param> /// <param name="author"></param> /// <returns></returns> private bool EditAuthor(XmlNode node, string author) { var versionNode = node.SelectSingleNode("Authors"); if (versionNode == null) { versionNode = node.AppendChild(node.OwnerDocument.CreateElement("Authors")); } versionNode.InnerText = author; return true; } /// <summary> /// 修改描述 /// </summary> /// <param name="content"></param> /// <param name="describe"></param> /// <returns></returns> private bool EditDescribe(XmlNode node, string describe) { var versionNode = node.SelectSingleNode("Description"); if (versionNode == null) { versionNode = node.AppendChild(node.OwnerDocument.CreateElement("Description")); } var index= versionNode.InnerText.IndexOf("-"); if (index != -1) { versionNode.InnerText = versionNode.InnerText.Replace(versionNode.InnerText.Substring(index, versionNode.InnerText.Length- index), "-"+describe); } else { versionNode.InnerText = versionNode.InnerText + "-" + describe; } return true; } }
GenerationUpload 生成並上傳 ,也是一個綜合操作的函數
使用XmlDocument去修改csproj中的信息,版本號、作者、描述
任務執行者:
public interface IDelivery { /// <summary> /// 服務庫(碼雲專用) /// </summary> /// <param name="gitAddress"></param> /// <param name="apiKey"></param> /// <param name="source"></param> /// <param name="changeCsFolders">更改文件列表</param> /// <param name="author">作者</param> /// <param name="describe">描述</param> /// <returns></returns> string MaYunServiceLibrary(string gitAddress, string apiKey, string source,List<string> changeCsFiles, string author,string describe); }
public class Delivery : IDelivery { IGitProcess gitProcess; INugetProcess nugetProcess; public Delivery(IGitProcess gitProcess,INugetProcess nugetProcess) { this.gitProcess = gitProcess; this.nugetProcess = nugetProcess; } public string MaYunServiceLibrary(string gitAddress, string apiKey, string source, List<string> changeCsFiles, string author, string describe) { List<string> csProjects = new List<string>(); StringBuilder message = new StringBuilder(); message.AppendLine(gitProcess.Obtain(gitAddress)); foreach (var item in changeCsFiles) { var csprojectItem = GetUpwardCsProject(new FileInfo(item).Directory); if (!csProjects.Contains(csprojectItem)) csProjects.Add(csprojectItem); } foreach (var item in csProjects) { message.AppendLine(nugetProcess.GenerationUpload(item, apiKey, source, author, describe)); } message.AppendLine(gitProcess.Push(gitAddress)); return message.ToString(); } private string GetUpwardCsProject(DirectoryInfo directoryInfo) { var file = directoryInfo.GetFiles("*.csproj"); if (file.Length > 0) { return file[0].FullName; } else { return GetUpwardCsProject(directoryInfo.Parent); } }
在這查找並過濾了重複的csproj文件
public interface IDeliveryTaskQueue { Queue<DeliveryTaskDTO> queue { get; } void AddQueue(DeliveryTaskDTO modle); }
public class DeliveryTaskQueue : IDeliveryTaskQueue { public Queue<DeliveryTaskDTO> queue { get; private set; } Thread TaskProcessing { get; } IDelivery delivery { get; } EventWaitHandle _waitHandle { get; } public DeliveryTaskQueue(IDelivery delivery) { queue = new Queue<DeliveryTaskDTO>(); this.delivery = delivery; _waitHandle = new AutoResetEvent(false); TaskProcessing = new Thread(Processing); TaskProcessing.Start(); } public void AddQueue(DeliveryTaskDTO modle) { if (modle.ChangeCsFiles.Count==0) { Console.WriteLine("當前請求沒有更改文件"); return; } queue.Enqueue(modle); _waitHandle.Set(); } private void Processing(object o) { while (true) { if (queue.Count==0) { Console.WriteLine("等待任務"); _waitHandle.WaitOne(); } else { var taskInfo = queue.Dequeue(); try { Console.WriteLine(delivery.MaYunServiceLibrary(taskInfo.GitAddress, taskInfo.ApiKey, taskInfo.Source, taskInfo.ChangeCsFiles, taskInfo.Author, taskInfo.Describe)); Console.WriteLine("任務處理完畢.."); } catch (Exception e) { Console.WriteLine("處理任務出現異常:"+e); } } } } }
由於可能會遇到併發提交的問題,所以需要一個隊列。
public static void ConfigureServices(IServiceCollection services) { services.AddTransient<IDeliveryTaskQueue, Dragon.Delivery.ServiceLibrary.Server.DeliveryTaskQueue>(); services.AddTransient<IDelivery, Dragon.Delivery.ServiceLibrary.Server.Delivery>(); services.AddTransient<IGitProcess, Dragon.Delivery.ServiceLibrary.Server.GitProcess>(); services.AddTransient<INugetProcess, Dragon.Delivery.ServiceLibrary.Server.NugetProcess>(); } public static void Configure(IApplicationBuilder app, IHostingEnvironment env) { }
再來就是WEB了
public class MaYunController : ControllerBase { IDeliveryTaskQueue deliveryTaskQueue; IGitProcess gitProcess; public MaYunController(IDeliveryTaskQueue deliveryTaskQueue, IGitProcess gitProcess) { this.deliveryTaskQueue = deliveryTaskQueue; this.gitProcess = gitProcess; } // GET api/values [HttpPost] public IActionResult MaYunHook(MaYunHookQo maYunHookQo) { foreach (var item in maYunHookQo.commits) { if (item.message.Contains("___Nuget打包任務___")) { //表明這次推送是打包推送的 continue; } if (item.message.Contains("Merge")&&item.message.Contains("branch")) { //表明這次推送是合併分支的請求 continue; } Console.WriteLine("收到打包任務:" + item.message + " 作者:" + item.author.name); List<string> changeCsFiles = new List<string>(); changeCsFiles.AddRange(item.removed.Where(x => x.Contains(".cs")).Select(x => $"{System.Environment.CurrentDirectory}\\{gitProcess.GetGitFolderMame(maYunHookQo.repository.clone_url)}\\{x}").ToList()); changeCsFiles.AddRange(item.added.Where(x => x.Contains(".cs")).Select(x => $"{System.Environment.CurrentDirectory}\\{gitProcess.GetGitFolderMame(maYunHookQo.repository.clone_url)}\\{x}").ToList()); changeCsFiles.AddRange(item.modified.Where(x => x.Contains(".cs")).Select(x => $"{System.Environment.CurrentDirectory}\\{gitProcess.GetGitFolderMame(maYunHookQo.repository.clone_url)}\\{x}").ToList()); deliveryTaskQueue.AddQueue(new ServiceLibrary.PublicServer.modle.DTO.DeliveryTaskDTO() { ApiKey = "71a6ab5d-308d-32c6-a7e8-25ff096f020a", Source = "http://x.x.x.x:8081/repository/nuget-hosted/", GitAddress = maYunHookQo.repository.clone_url, Author = item.author.name, Describe = item.message, ChangeCsFiles = changeCsFiles }); } return Ok(); } }
從碼雲的請求中獲取信息然後提交到任務隊列
那個請求模型就不貼了,代碼太多了,可以自行獲取
然後到json轉C#實體的網站上
https://www.sojson.com/json2entity.html做個轉換就行啦。
以上:使用持續交付減少開發流程