龙空技术网

持续交付之解决Jenkins集成编译获取代码提交记录及钉钉通知

zuozewei 1669

前言:

而今姐妹们对“isempty源码”大约比较着重,咱们都想要剖析一些“isempty源码”的相关内容。那么小编也在网摘上汇集了一些有关“isempty源码””的相关资讯,希望各位老铁们能喜欢,各位老铁们一起来了解一下吧!

一、背景

我们在使用 Jenkins 集成编译完成后,会主动向项目组钉钉群推送编译和打包通知,方便测试同学下载测试。但同时带来一个新的需求,项目组同学想从通知中快速了解代码变更内容。我们知道开发同学在 Git 提交代码的时候都有修改注释,所以思考是否能直接获取代码的修改注释显示在最终的编译完成的通知中,直观显示给项目组每个人。

二、解决方案

搜索了一遍发现 Jenkins 没有官方插件来解决这个问题,倒是有一个老外开源的插件可用。

落地方案:

Jenkins Changelog Environment Plugin 实现获取代码提交记录使用 Python 获取代码提交记录并推送钉钉通知1、下载插件源码

Git Clone 插件源码

2、源码简要分析

代码骨架结构如下:

└─src    ├─main    │  ├─java    │  │  └─org    │  │      └─jenkinsci    │  │          └─plugins    │  │              └─changelogenvironment    │  │                      ChangelogEnvironmentContributor.java  # 插件具体实现类    │  │    │  └─resources    │      │  index.jelly  # 插件的概要描述,可以在插件列表中看到    │      │    │      └─org    │          └─jenkinsci    │              └─plugins    │                  └─changelogenvironment    │                      │  Messages.properties  #GUI配置文件    │                      │  Messages_de.properties #GUI配置文件    │                      │    │                      └─ChangelogEnvironmentContributor    │                              config.jelly             # 插件在单个job中的配置    │                              config_de.properties     # 配置文件    │                              help-dateFormat.html  # jelly文件中属性的帮助文件    │                              help-dateFormat_de.html  # 同上    │                              help-entryFormat.html # 同上    │                              help-entryFormat_de.html # 同上    │                              help-lineFormat.html  # 同上    │                              help-lineFormat_de.html # 同上    │    └─test        └─java            └─org                └─jenkinsci                    └─plugins                        └─changelogenvironment                                ChangelogEnvironmentContributorTest.java  # 单元测试类│  pom.xml

index.jelly: 插件的概要描述,可以在插件列表中看到

<?jelly escape-by-default='true'?><div>  This plugin allows you to add changelog information to the build environment for use in build scripts.</div>

Jenkins 展示效果:

pom.xml:name 属性值就是插件管理页面中的插件名称,如下:

<name>Changelog Environment Plugin</name>

*html:jelly文件中属性的帮助文件,点击插件的“?”即可展示:

<p>This field specifies the Java <code>String</code> format of the changelog entry header. It accepts four <code>String</code> placeholders:   <ol>       <li>The author of the commit</li>       <li>The ID or revision of the commit</li>       <li>The commit message</li>       <li>The date of the commit in the format specified in the <em>Date Format</em> field</li>   </ol>   After this commit information, a list of affected paths will be printed in the <em>File Item Format</em>.</p>

帮助文件在 Jenkins GUI 效果如下:

接下来我们来看下 ChangelogEnvironmentContributor.java

public class ChangelogEnvironmentContributor extends SimpleBuildWrapper {   private String entryFormat;   private String lineFormat;   private String dateFormat;   @DataBoundConstructor   public ChangelogEnvironmentContributor() {       // need empty constructor so Stapler creates instances   }   @DataBoundSetter   public void setDateFormat(String dateFormat) {       this.dateFormat = dateFormat;   }   @DataBoundSetter   public void setEntryFormat(String entryFormat) {       this.entryFormat = entryFormat;   }   @DataBoundSetter   public void setLineFormat(String lineFormat) {       this.lineFormat = lineFormat;   }   public String getEntryFormat() {       return this.entryFormat;   }   public String getLineFormat() {       return this.lineFormat;   }   public String getDateFormat() {       return this.dateFormat;   }   @Override   public void setUp(Context context, Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener, EnvVars initialEnvironment) throws IOException, InterruptedException {       StringBuilder sb = new StringBuilder();       DateFormat df;       try {           df = new SimpleDateFormat(Util.fixNull(dateFormat));       } catch (IllegalArgumentException ex) {           listener.error("Failed to insert changelog into the environment: Illegal date format");           return;       }       try {           if (build instanceof AbstractBuild<?, ?>) {               AbstractBuild<?, ?> abstractBuild = (AbstractBuild<?, ?>) build;               ChangeLogSet cs = abstractBuild.getChangeSet();               processChangeLogSet(sb, cs, df);           }           try {               // FIXME TODO I have no idea whether this works, untested               if (build instanceof WorkflowRun) {                   WorkflowRun wfr = (WorkflowRun) build;                   List<ChangeLogSet<? extends ChangeLogSet.Entry>> changeLogSets = wfr.getChangeSets();                   for (ChangeLogSet<? extends ChangeLogSet.Entry> changeLogSet : changeLogSets) {                       processChangeLogSet(sb, changeLogSet, df);                   }               }           } catch (NoClassDefFoundError ncder) {               // ignore           }       } catch (IllegalFormatException ex) {           listener.error("Failed to insert changelog into the environment: " + ex.getMessage());           return;       }       String value = sb.toString();       if (!"".equals(value)) {           context.env("SCM_CHANGELOG", value);       }   }   private void processChangeLogSet(StringBuilder sb, ChangeLogSet cs, DateFormat df) {       for (Object o : cs) {           ChangeLogSet.Entry e = (ChangeLogSet.Entry) o;           sb.append(String.format(Util.fixNull(this.entryFormat), e.getAuthor(), e.getCommitId(), e.getMsg(), df.format(new Date(e.getTimestamp()))));           try {               for (ChangeLogSet.AffectedFile file : e.getAffectedFiles()) {                   sb.append(String.format(Util.fixNull(this.lineFormat), file.getEditType().getName(), file.getPath()));               }           } catch (UnsupportedOperationException ex) {               // early versions of SCM did not support getAffectedFiles, only had getAffectedPaths               for (String file : e.getAffectedPaths()) {                   sb.append(String.format(Util.fixNull(this.lineFormat), "", file));               }           }       }   }   @Extension   public static class ChangelogEnvironmentContributorDescriptor extends BuildWrapperDescriptor {       @Override       public boolean isApplicable(AbstractProject<?, ?> item) {           // only really makes sense for jobs with SCM, but cannot really not show this option otherwise           // users would have to leave the config form between setting up SCM and this.           return true;       }       @Override       public String getDisplayName() {           return Messages.DisplayName();       }       public FormValidation doCheckLineFormat(@QueryParameter String lineFormat) {           try {               String result = String.format(lineFormat, "add", "README.md");               return FormValidation.ok(Messages.LineFormat_Sample(result));           } catch (IllegalFormatException ex) {               return FormValidation.error(Messages.LineFormat_Error());           }       }       public FormValidation doCheckEntryFormat(@QueryParameter String entryFormat) {           try {               String result = String.format(entryFormat, "danielbeck", "879e6fa97d79fd", "Initial commit", 1448305200000L);               return FormValidation.ok(Messages.EntryFormat_Sample(result));           } catch (IllegalFormatException ex) {               return FormValidation.error(Messages.EntryFormat_Error());           }       }       public FormValidation doCheckDateFormat(@QueryParameter String dateFormat) {           try {               String result = new SimpleDateFormat(dateFormat).format(new Date(1448305200000L));               return FormValidation.ok(Messages.DateFormat_Sample(result));           } catch (IllegalArgumentException ex) {               return FormValidation.error(Messages.DateFormat_Error());           }       }   }}
ChangelogEnvironmentContributor:继承 SimpleBuildWrapper,是 BuildWrapper 的一个公共抽象类,其是可扩展点,用于执行构建过程的前 / 后操作,比如准备构建的环境,设置环境变量等实现 setUp 方法,主要为获取代码提交记录,几个参数: Context context:当前上下文Run build:当前构建任务FilePath workspace:文件路径Launcher launcher:启动构建TaskListener listener:检查构建状态EnvVars initialEnvironment:环境变量@DataBoundConstructor:插件的构造函数,此处留空以便 Stapler 创建实例@DataBoundSetter:标识插件属性的 setter 方法@Extension:扩展点注释,Jenkins 通过此注解自动发现扩展点,并加入扩展列表

判断构建是否为基本实例,AbstractBuild 是基本构建实例

  if (build instanceof AbstractBuild<?, ?>) {               AbstractBuild<?, ?> abstractBuild = (AbstractBuild<?, ?>) build;               ChangeLogSet cs = abstractBuild.getChangeSet();               processChangeLogSet(sb, cs, df);           }

获取合并到当前构建中的所有更改信息

ChangeLogSet cs = abstractBuild.getChangeSet();

判断构建是否为工作流实例,WorkflowRun 是工作流实例,类似 pipeline?(此处没有查到 API),这里作者说可能失效,未经过验证。

// FIXME TODO I have no idea whether this works, untested                if (build instanceof WorkflowRun) {                    WorkflowRun wfr = (WorkflowRun) build;                    List<ChangeLogSet<? extends ChangeLogSet.Entry>> changeLogSets = wfr.getChangeSets();                    for (ChangeLogSet<? extends ChangeLogSet.Entry> changeLogSet : changeLogSets) {                        processChangeLogSet(sb, changeLogSet, df);                    }                }

getChangeSets 为获取当前与此项关联的所有更改日志

  List<ChangeLogSet<? extends ChangeLogSet.Entry>> changeLogSets = wfr.getChangeSets();

processChangeLogSet:自定义处理当前更改日志,主要为格式化和拼接日志

    private void processChangeLogSet(StringBuilder sb, ChangeLogSet cs, DateFormat df) {        for (Object o : cs) {            ChangeLogSet.Entry e = (ChangeLogSet.Entry) o;            sb.append(String.format(Util.fixNull(this.entryFormat), e.getAuthor(), e.getCommitId(), e.getMsg(), df.format(new Date(e.getTimestamp()))));            try {                for (ChangeLogSet.AffectedFile file : e.getAffectedFiles()) {                    sb.append(String.format(Util.fixNull(this.lineFormat), file.getEditType().getName(), file.getPath()));                }            } catch (UnsupportedOperationException ex) {                // early versions of SCM did not support getAffectedFiles, only had getAffectedPaths                for (String file : e.getAffectedPaths()) {                    sb.append(String.format(Util.fixNull(this.lineFormat), "", file));                }            }        }    }

最后变更日志赋值给 SCM_CHANGELOG 变量

  String value = sb.toString();       if (!"".equals(value)) {           context.env("SCM_CHANGELOG", value);       }

ChangelogEnvironmentContributorDescriptor:配置类,这里主要对GUI输入进行格式验证,通过 config.jelly 配置文件进行参数设置:

<?jelly escape-by-default='true'?><j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form">   <f:entry title="${%Entry Format}" field="entryFormat">       <f:textarea/>   </f:entry>   <f:entry title="${%File Item Format}" field="lineFormat">       <f:textarea/>   </f:entry>   <f:entry title="${%Date Format}" field="dateFormat">       <f:textbox/>   </f:entry></j:jelly>
doCheckLineFormat:文件验证doCheckEntryFormat:变更日志验证doCheckDateFormat:时间格式验证3、编译源码生成 hpi

切换到程序根目录 changelog-environment-plugin-master 下,执行:

mvn verify

等待一段比较长的时间,会在 ./target/ 下生成一个 changelog-environment.hpi 文件,这个就是我们需要的插件

把生成的插件文件上传到 Jenkins

4、Jenkins Job设置

项目的配置中,构建环境下面多了一项Add Changelog Information to Environment,如下图:

Entry Format :添加 - %3$s (%4$s %1$s) ,参数分别为 ChangeLog、时间、提交人Date Format :添加 yyyy-MM-dd HH:mm:ss 就是时间格式

顺序依据如下:

 sb.append(String.format(Util.fixNull(this.entryFormat), e.getAuthor(), e.getCommitId(), e.getMsg(), df.format(new Date(e.getTimestamp()))));
5、钉钉通知

通过 os.getenv("SCM_CHANGELOG") 获取变更日志内容

完整 Python 脚本如下:

# coding=utf-8'''@author: zuozewei@file: notification.py@time: 2019/4/25 18:00@description:dingTalk通知类'''import os, jenkins, configparser, requests, json, timefrom dingtalkchatbot.chatbot import DingtalkChatbotfrom jsonpath import jsonpath# 获取Jenkins变量JOB_NAME = str(os.getenv("JOB_NAME"))BUILD_URL = str(os.getenv("BUILD_URL")) + "console"BUILD_VERSION = str(os.getenv("BUILD_VERSION"))JENKINS_HOME = os.getenv("JENKINS_HOME")BUILD_NUMBER = str(os.getenv("BUILD_NUMBER"))SCM_CHANGELOG = str(os.getenv("SCM_CHANGELOG"))WORKSPACE = os.getenv("WORKSPACE")versionPath = JENKINS_HOME + "\workspace\Version.ini"# 读取版本号config = configparser.ConfigParser()config.read(versionPath)xxx_Major = config.get("xxx", "xxx_Major")xxx_Minor = config.get("xxx", "xxx_Minor")xxx_Build = config.get("xxx", "xxx_Build")xxx_Revision = config.get("xxx", "xxx_Revision")VERSION = xxx_Major + "." + xxx_Minor + "." + xxx_Build+ "." + xxx_Revision # 判断日志内容if SCM_CHANGELOG == 'None':    SCM_CHANGELOG = '- No changes'    print("empty")else:    print("not empty")    passdef buildNotification():    title = 'xxx编译通知'    # 连接jenkins    server1 = jenkins.Jenkins(url=";, username='xxx', password="xxx")    build_info = server1.get_build_info(JOB_NAME, int(BUILD_NUMBER))    # dict字典转json数据    build_info_json = json.dumps(build_info)    # 把json字符串转json对象    build_info_jsonobj = json.loads(build_info_json)    # 获取任务触发原因    causes = jsonpath(build_info_jsonobj, '$.actions..shortDescription')    print(causes[0])    textFail = '#### ' + JOB_NAME + ' - Build # ' + BUILD_NUMBER + ' \n' + \               '##### <font color=#FF0000 size=6 face="黑体">编译状态: ' + BUILD_STATUS + '</font> \n' + \               '##### **版本类型**: ' + '开发版' + '\n' + \               '##### **当前版本**: ' + VERSION + '\n' + \               '##### **触发类型**: ' + str(causes[0]) + '\n' + \               '##### **编译日志**:  [查看详情](' + BUILD_URL + ') \n' + \               '##### **关注人**: @186xxxx2487 \n' + \               '##### **更新记录**: \n' + \               SCM_CHANGELOG + '\n' + \               '> ###### xxx技术团队 \n '    textSuccess = '#### ' + JOB_NAME + ' - Build # ' + BUILD_NUMBER + ' \n' + \                  '##### **编译状态**: ' + BUILD_STATUS + '\n' + \                  '##### **版本类型**: ' + '开发版' + '\n' + \                  '##### **当前版本**: ' + VERSION + '\n' + \                  '##### **触发类型**: ' + str(causes[0]) + '\n' + \                  '##### **编译日志**: [查看详情](' + BUILD_URL + ') \n' + \                  '##### **更新记录**: \n' + \                  SCM_CHANGELOG + '\n' + \                  '> ###### xxx技术团队 \n '    if BUILD_STATUS == 'SUCCESS':        dingText = textSuccess    else:        dingText = textFail            sendding(title, dingText)def sendding(title, content):    at_mobiles = ['186xxxx2487']    Dingtalk_access_token = ';    # 初始化机器人小丁    xiaoding = DingtalkChatbot(Dingtalk_access_token)    # Markdown消息@指定用户    xiaoding.send_markdown(title=title, text=content, at_mobiles=at_mobiles)    if __name__ == "__main__":    buildNotification()

最终几种通知的效果如下:

三、小结

本文带着大家从 Jenkins 插件源码到应用走了一遍,可能有些地方描述还不是很具体,不过针对大部分的 Jenkins 插件,这个套路是类似的,希望大家能有所启发。

本文源码:

标签: #isempty源码