思考一种好的架构(十四)
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做个转换就行啦。
以上:使用持续交付减少开发流程