cover_image

AirMax -Android 组件修改信息设计落地

lovejjfg 得物技术 2022年01月05日 10:31


图片


背景 

在 Android 当前整个 CI / CD 流程中,组件或者 App 产物产出时缺少核心修改信息。

由于修改信息的缺失,这会让开发困惑于该产物是否包含我最新的代码提交。

这也给开发/测试流程带来了诸多不便,例如测试不清楚当前需要使用哪个App验证回归相关问题,开发在修完Bug后,也只能手动通知到测试需要使用某个时间节点后的App包,增加了不必要的沟通环节,降低了开发测试验收的效率。


技术规划

为了解决这个问题,AirMax 项目成立,一个核心,记录每一个组件的修改 commit 信息,两个展示,TestFilght 和 得物App 均要支持修改信息的展示,每个App和组件都可追溯修改记录。


为了完成这个目标,涉及到的工作有 CI 卡口 commit 检查优化,deploy 发布后增加 log 拉取存储服务,App 打包流程找那个增加apk包信息存入,便于在 App 中能拉取到自己的修改信息。

最后是 TestFlight app 改版,使用新的接口完成列表和详情页的网络请求。



整体流程

图片



deploy 服务

图片



App 打包

图片


CI  卡口优化

主要是针对 commit 信息检查的优化,之前只是简单校验 commit 信息是否是 feat fix 等开头,但是发现很多同学,因为卡口造成了冗余的提交,并且没有对此进行commit精简或者合并。

为了优化这个情况,保证后续展示的 commit 信息是精准的,做了以下规则调整:

  1. 格式化冗余信息(包含「代码格式化」等信息的 commit)

  2. commit 修改文件数量小于 commit 数量

  3. 包含重复的 commit 信息


# 检查是否有重复的提交信息commitInfoIsRepeatCheck() {  commitInfoArr=$1  for ((i = 1; i <= ${#commitInfoArr[@]}; i++)); do    for ((j = i + 1; j <= ${#commitInfoArr[@]}; j++)); do      if [[ ${commitInfoArr[i]} == ${commitInfoArr[j]} ]]; then        notifyError "MR存在完全重复的commit信息" "MR存在完全重复commit ${commitInfoArr[i]},为优化commit质量,请本地进行commit的相关合并操作\n参考文档:$commitRuleUrl" "$ppUrl"        notifyErrorToComponentGroup "commit重复异常" "用户:$userName 邮箱:$GITLAB_USER_EMAIL commit完全重复异常" "$ppUrl"        exit 1      fi    done  done  echo "无重复的commit信息,符合commit规范"}

#获取提交总计改动的文件的数量getChangedFilesCount() {  commitCount=$1  # shellcheck disable=SC2034  changeInfo=$(git diff --shortstat HEAD~${commitCount} HEAD)  echo "changeInfo---$changeInfo"  filesCount=$(echo ${changeInfo%file*} | sed 's/ //g')  echo "changeFilesCount-----$filesCount"  echo "commitCount-----$commitCount"  if [ $filesCount -ge $commitCount ]; then    echo "改动文件数量符合规范"  else    notifyError "commit数量异常" "存在同个文件提交重复commit的情况,为优化commit质量,请本地进行commit的相关合并操作 \n参考文档:$commitRuleUrl" "$ppUrl"    notifyErrorToComponentGroup "commit数量异常" "用户:$userName 邮箱:$GITLAB_USER_EMAIL commit提交数量超过了改动文件个数" "$ppUrl"    exit 1  fi}


核心 commit 信息获取


关键参数

要记录 commit 信息,首先要先认识一下 log 信息,这个比较重要,因为有很多坑在里面。

一条标准的开发分支,可能就是一个链表的结构。

但是它还有 Merge 等操作,多条 分支线,又同步到一条。

有这个概念之后,我们看下官方提供的 API ,可能也能发现一些端倪。

图片

id不用讲,就是 project id ,这个是必传。

接着有一个 ref_name ,这个是指定分支名称,这个我们也需要使用,毕竟我们是需要拉取某一个具体分支的某一个打包的修改记录,接下来两个是时间,since ,从这个时间开始;until,到这个时间结束;path,不用关心;all ,也不用全部拉取;with_stats,这个也不用关心。 

first_parent ,这个参数一开始是让我感到困惑,看描述信息应该和 merge 时多个分支合成一个有关。 

order,按默认排序就行。

trailers,我们没有设置,也不需要带上。


那我们必须使用的参数有 id ref_name since unitl ,即我们要确定在同一个分支本地打包时间到上次打包的修改信息。

图片

这是一条 API 返回的标准 Log 信息,有几个概念要补充下,这里有 title 和 message ,可以看到,message 是包含注释信息,是commit 中的所有内容,当然也包含我们自动加入的分支信息。

对于这里来说,使用 title 足矣。


另外,还有三个时间,created_at authored_at committer_date 这三个到底是啥区别呢?


查阅相关资料,authored_at 是这个 commit 最早创建的时间,后面进行 rebase 等修改操作,修改的是  committer_date

created_at 则与 committer_date 保持一致。

You may be wondering what the difference is between author and committer. The author is the person who originally wrote the work, whereas the committer is the person who last applied the work. So, if you send in a patch to a project and one of the core members applies the patch, both of you get credit — you as the author, and the core member as the committer. We’ll cover this distinction a bit more in Distributed Git(http://git-scm.com/book/en/v2/Distributed-Git-Distributed-Workflows#ch05-distributed-git)。

既然这样,结合上面的 api ,我们记录肯定需要使用 committer_date ,那在git 中,怎么直接获取 committer_date 呢?


再查阅资料,https://git-scm.com/docs/git-log ,找到了 --format 中的指定方式。

%ct

committer date, UNIX timestamp

这里就使用时间戳的时间形式将发布的当前 commit 时间记录,在下次继续发布时,取出上一次的发布时间,通过 since 和 until 确定好本次的 commit 修改区间。



异常情况

重复构建

如果,一个组件没有额外修改信息被重复构建了多次,那么,这几次的修改信息应该是同一条。


if (lastPublishComponentVersion != null && Objects.equals(lastPublishComponentVersion.getCommitId(), componentVersion.getCommitId())) { logger.info("fetchGitCommitLogs cur commitId equals last commitId, copy data"); List<GitCommitLog> lastGitCommitLogs = gitCommitLogMapper.list(new HashMap<String, Object>() {{ put("cvId", lastPublishComponentVersion.getId()); }}); lastGitCommitLogs.forEach(item -> { item.setId(null); item.setCvId(componentVersion.getId()); }); gitCommitLogs = lastGitCommitLogs;}

since 异常

组件第一次构建时,肯定没有上个打包时间,这个时候,就选择开始时间的24小时前,然后取三条信息存入即可。


如果直接使用上一次的 commit date 信息,本次信息拉取会包含上次的最后一条信息,所以,使用 since 请求时,要手动加上 1001 ms 作为偏移(并发合入的情况几乎不存在)。


if (lastPublishComponentVersion == null) {    since = ISO8601Utils.format(new Date(componentVersion.getCommitTime() - 24 * 60 * 60 * 1000), true);    count = paramConfigService.getIntValue(ParamConfigKeys.CV_GITLOG_FETCH_COUNT, 3);} else {    since = ISO8601Utils.format(new Date(lastPublishComponentVersion.getCommitTime() + 1001), true);}

commit 信息丢失

我们提交 MR 时,由于是多人并行开发,总会出现,时间合入落后的情况。

这个时候就会存在一种情况。

一次 MR 中,真实的 commit 时间落在了上次发布时间外,这个时候,通过当前生成的 Merge into 新 commit 信息作为 until 去查询的话,since 时间大于了 正式 commit 时间。所以就查不出此条 commit 信息。


这个时候怎么处理呢?不要忘记上面讲到的 parent_ids 参数,它其实就记录了自己父节点,那这正好就是我们需要的呢。

图片

这是一条标准的 Merge commit 信息,可以看到 parent id 对应两条,第一条永远对应主干分支的上一个 commit ,第二个,就是合入分支的 上一个 commit ,这个就是我们想要的 commit id。


通过这个 commit_id ,请求一个单独的log信息即可。

图片

通过这次优化,可以处理时间异常的 commit 信息抓取。

当然,这样只抓取了一条信息,可能存在丢失信息的问题,不过由于我们有CI前置卡口,MR 目前要求一个 MR 就做一件事,最好只有一个 commit 。

所以这种极端情况,有一条信息,也能确定相关代码被打包到 App 中。


fetchUrl = String.format("https://gitlab.poizon.com/api/v4/projects/%d/repository/commits?since=%s&until=%s&ref_name=%s", repo.getProjectId(), since, until, componentVersion.getDeployBranch());gitCommitLogs = fixWhenOnlyOneMerge(repo, fetchGitCommitLogs(fetchUrl, count));if (gitCommitLogs == null || gitCommitLogs.size() == 0) {    fetchUrl = String.format("https://gitlab.poizon.com/api/v4/projects/%d/repository/commits?until=%s&ref_name=%s", repo.getProjectId(), until, componentVersion.getDeployBranch());    gitCommitLogs = fetchGitCommitLogs(fetchUrl, count);    if (gitCommitLogs != null && !gitCommitLogs.isEmpty()) {        final GitCommitLog firstCommitLog = gitCommitLogs.get(0);        gitCommitLogs = fixWhenOnlyOneMerge(repo, new ArrayList<GitCommitLog>(1) {{            add(firstCommitLog);        }});    }}

private List<GitCommitLog> fixWhenOnlyOneMerge(Repo repo, List<GitCommitLog> gitCommitLogs) {    if (gitCommitLogs == null || gitCommitLogs.size() != 1) {        return gitCommitLogs;    }    GitCommitLog firstGitCommitLog = gitCommitLogs.get(0);    if (firstGitCommitLog.getParent_ids() == null || firstGitCommitLog.getParent_ids().size() != 2) {        return gitCommitLogs;    }    String parentId = firstGitCommitLog.getParent_ids().get(firstGitCommitLog.getParent_ids().size() - 1);    String fetchUrl = String.format("https://gitlab.poizon.com/api/v4/projects/%d/repository/commits/%s", repo.getProjectId(), parentId);    logger.info("fixWhenOnlyOneMerge url: " + fetchUrl);    Request request = new Request.Builder().url(fetchUrl).header("PRIVATE-TOKEN", "xxxxxxxxxxxxx").build();    try {        Response response = getOkHttpClient().newCall(request).execute();        if (response.code() == 200) {            String json = response.body().string();            logger.info("fixWhenOnlyOneMerge json: " + json);            JSONObject jsonObject = JSONObject.parseObject(json);            gitCommitLogs.add(jsonObjectToGitCommitLog(jsonObject));        } else {            throw new RuntimeException("fixWhenOnlyOneMerge code != 200");        }        return gitCommitLogs;    } catch (IOException e) {        logger.error("fixWhenOnlyOneMerge", e);        throw new RuntimeException(e);    }}


App 构建优化

App 构建流程主要是在构建成功后,需要进行组件信息的拉取以及聚合,然后再根据 App 的 md5 值为key进行落库存储。

其中 ,md5 还需要通过 瓦力工具,更新到 App 中,这样,App才能在启动后,通过改字段去拉取到自己的Log信息。


def generateChannelApk(File apkFile, File channelOutputFolder, Map nameVariantMap, channel, extraInfo, alias) {    //poizon modify    Map apkFileMd5Cache = nameVariantMap.computeIfAbsent("apkFileMd5Cache", { k -> new HashMap<String, String>()});    def apkFileMd5 = apkFileMd5Cache.computeIfAbsent(apkFile.absolutePath, { k ->        String md5 = MD5Utils.md5(IOUtils.toString(new BufferedInputStream(new FileInputStream(apkFile))));        println("add gitCommitLogKey sourceApk: ${apkFile} -> ${md5}")        return md5;    });    if (extraInfo == null) {        extraInfo = new HashMap<String, String>()    }    extraInfo.put("componentLogId", apkFileMd5)    nameVariantMap.put("sourceApkMd5", apkFileMd5);    println("----put extra componentLogId: ${apkFileMd5}----")    Extension extension = Extension.getConfig(targetProject);
   ...    WallePlusExtUtils.putChannelOutput(targetProject, variant, nameVariantMap)}


TestFlight 优化

Testflight 的改动主要涉及列表逻辑重构,之前是直接使用 OSS 的 SDK 拉取数据。

这次通过新做的接口完成统一处理。由于一个版本apk信息过多,为了减轻列表接口的负担,只做前50个apk的 log信息拉取,后面的只支持在详情中查看 Log 信息。


由于新增点击跳转详情页,TestFlight 交互也有调整,主要涉及点击安装逻辑的调整。

另外引入了 得物App中的网络库,后续将统一 TestFlight 和 得物App 的开发环境,实现代码通用。


小结

通过本次任务,更加深刻了解 git 的一些知识,顺利落地组件信息展示,做到每个App可溯源,改动可感知。

同时也为我们的CI前置卡口,大家的日常代码提交反馈出更细致的问题,敦促大家严格要求自己,提高代码提交规范,为测试,开发反馈提供有效支持,为持续集成和交互提供有力保障。


图片

*文/lovejjfg


微信扫一扫
关注该公众号

继续滑动看下一个
得物技术
向上滑动看下一个