思考一種好的架構(十四)

 

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做個轉換就行啦。

以上:使用持續交付減少開發流程

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章