内森·沃尔特斯(Nathan Walters)

在NerdWallet,我们已经完全接受了现代微服务架构。 我们的网站由数十个React应用组成,这些应用从各种内部服务中提取内容和数据。 这种工作方式具有明显的好处:团队可以独立工作,快速迭代并通过故障隔离频繁部署。 当使用这么多代码库时,希望将常见组件提取到可重用的包中:我们拥有共享库,这些库提供服务器端渲染,数据获取,缓存,React组件等。 这对于提高设计一致性和减少需要进入新应用程序的样板数量非常有用。 但是,这会使更新共享库变得困难:
- 更新图书馆的人必须将更改通知图书馆的使用者
- 消费者必须了解如何进行更改并随后更新其代码
- 使用者必须测试,合并和部署这些更改
即使是相对较小的更改,此过程也很容易在我们所有团队中花费数百个工时。 这个确切的问题在最近来自Segment的广泛分享的博客文章中提到,他们讨论了为什么他们重新回到整体市场:
测试和部署对这些共享库的更改影响了我们的所有目的地。 它开始需要大量的时间和精力来维护。 进行更改以改进我们的库,知道我们必须测试和部署数十种服务,这是一个冒险的提议。”
当共享代码更改时,更新应用程序所需的时间就是可以在我们的产品上进行创新的时间。 作为一名工程师,我的自然志向是使一切可能自动化的东西都自动化。
Facebook的jscodeshift是一个在代码库中自动执行代码更改的工具的很好的例子。 一个人可以编写一个在文件级别进行更改的“ codemod”,然后jscodeshift将该代码jscodeshift应用于代码库中的所有适用文件。 但是,这仍然需要一个人做很多手工工作:他们必须确定要更改的存储库,将它们克隆到自己的计算机上,进行更改,提交并推送更改,并为每个存储库打开拉取请求。 对于需要在数百个存储库中进行的更改,这仍然不是效率的巨大提升。
发现哪些代码需要更改,进行更改并将更改提交给团队进行审核的过程不必手动进行; 它也是自动化的理想选择。 输入牧羊人。
牧羊人
Shepherd是NerdWallet开发的开源CLI工具,用于协调我们所有存储库中代码更改的应用,从签出,进行更改到提交拉取请求。 可以使用任何您喜欢的语言或工具执行迁移,而Shepherd与存储库中使用的语言无关。 它也与所使用的版本控制系统的类型无关:它附带了对GitHub的支持,但是与存储库的所有交互都已抽象为通用适配器,因此可以轻松添加对其他服务(如GitLab或Bitbucket)的支持。
迁移以声明方式写入YAML文件。 各个步骤被编写为Shell命令,这意味着您可以在需要的时候使用自己喜欢的Unix工具,但也可以根据需要使用node或python调用更复杂的脚本。 您可以使用任意代码来决定哪些存储库需要迁移,并生成可能复杂的提取请求消息。
一个简单的例子
ESLint已弃用无扩展名.eslintrc配置文件,转而使用诸如.eslintrc.yml或.eslintrc.json的显式扩展名。 但是,我们所有的JavaScript应用都是使用.eslintrc文件的模板创建的。 为了使我们的应用程序保持最新,我们希望将这些文件重命名为.eslintrc.yml 。 对于一个存储库来说,这是一个足够容易的更改,但是对于任何一名工程师而言,在80多个相关存储库中进行手动更改将花费大量时间。 幸运的是,借助Shepherd,可以轻松实现整个过程的自动化。 这是此过程的迁移规范的简化版本:
让我们来看这个例子。 id指定此迁移的唯一标识符,Shepherd在内部使用它来跟踪状态以及分支名称。 title用于构建提交消息并提取请求标题。 adapter指定应使用的版本控制适配器,以及该适配器的选项。 此示例使用github适配器,并且使用GitHub的代码搜索限定符查找根目录中具有.eslintrc文件的存储库。 包含与该查询匹配的文件的任何存储库都将被视为此迁移的候选对象。
挂钩部分允许您定义在应用迁移过程中Shepherd将调用的“生命周期”挂钩。 should_migrate挂钩可让您在检出存储库后执行其他存储库过滤。 在这种情况下,我们会进行完整性检查以确保.eslintrc实际上存在,并且还要检查该存储库的最新提交是在2018年。我们这样做是为了避免在可能不再维护或使用的旧存储库上产生噪音。 如果这些命令中的任何一个以非零退出代码退出,则该检查被视为失败,并且存储库将不会应用迁移。
apply钩子指定要实际将迁移应用于每个存储库的命令。 在这里,我们只是使用mv重命名文件。 但是,此步骤可能要复杂得多,稍后我们将进行介绍。
最后, pr_message挂钩允许您动态生成将用于每个请求请求的消息。 在这种情况下,该消息是一个简单的静态字符串,但是在更复杂的迁移中,您可以构建一条消息,列出可能需要人为注意的特定事物,例如可能需要主要版本变更的特定依赖项。 这个钩子将在每个步骤中取出所有写入标准的东西,并将输出连接到拉取请求消息中。
而已! 现在,我们只需要五个简单的命令即可从检出相关存储库到提交拉取请求。 假设您的shepherd.yml规范位于名为eslintrc-migration的目录中,这就是您要运行的:
您的存储库现在将具有拉取请求,等待审核。

一个复杂的用例:升级到React 16
Shepherd的第一个主要用例是在将应用程序更新到React 16的过程中。我们希望我们的开发人员能够利用最新和最大的React功能。 但是,我们有很多React应用程序和组件,手动更新它们以使其与React 16兼容是一项艰巨的任务。 我们认为这是一项至少可以很容易实现自动化的任务:诸如更新依赖关系和运行某些React codemod之类的事情很容易自动完成。 有些事情不能完全自动化。 例如,我们的某些内部React组件需要进行重大更改以实现React 16兼容性,并且我们不能总是始终自动更新这些组件的用法。 但是,我们能够确定需要主要版本变更的依赖项,并在拉出请求中专门将其删除。 这使最终不得不审查和测试拉动请求的人员更加容易。
这是我们用于构建此迁移的大多数迁移规范:
在我们之前的示例中,所有迁移逻辑都位于迁移规范本身中。 这个React 16示例展示了如何使用其他工具(npm中的CLI,Node脚本和Shell脚本)来构建复杂的逻辑。 您会注意到一些apply命令包含$SHEPHERD_MIGRATION_DIR 。 默认情况下,每个命令的工作目录都设置为正在使用的存储库的根目录。 这使得在每个存储库中使用Unix实用程序变得容易,但这意味着我们需要知道可能与shepherd.yml规范一起使用的辅助脚本的绝对路径。 Shepherd通过$SHEPHERD_MIGRATION_DIR环境变量公开包含shepherd.yml文件的目录的绝对路径,从而允许您运行辅助脚本。
这也是能够以编程方式生成有用的拉取请求消息的一个很好的例子。 通过查看我们的依赖关系以及这些依赖关系的依赖关系,我们可以找出哪些软件包需要主要版本更新,哪些软件包无法自动更新。 然后,我们创建了待办事项列表,以列出这些程序包,然后审阅者可以使用它们来跟踪其进度。

该示例及其引用的支持脚本在Shepherd GitHub存储库中可见。 尽管它包含大量NerdWallet特定的代码,包括对称为nw-react的私有软件包的引用,该私有软件包包含一些内部工具,但它应该有助于说明Shepherd的功能。
结论
当我今年夏天第一次开始在Shepherd上工作时,我还没有完全意识到这种工具可能对完成工作的方式产生的影响,我的团队也没有其他人。 构建该工具并在NerdWallet中使用它花费了几个月的时间,才能真正内部化它可能产生的影响。 现在,在办公室周围听到人们问“我们可以用Shepherd自动化它吗?”的情况并不少见。 希望这篇博客文章能使您开始提出同样的问题。
Shepherd当然不是万能的解决方案。 如果您没有很多使用代码的地方,那么写一个自动更改可能比花更多的麻烦。 当然,也存在某些类别的问题比其他类别更适合自动化。 例如,Shepherd不太适合执行复杂的重构。 还值得注意的是,monorepos也可以帮助减轻使这类工具首先成为必需的问题。 但是,还有许多适合自动化的问题,Shepherd可以帮助您解决这些问题,同时保持微服务架构的优势。
尽管我们已经在NerdWallet上成功使用了几个月,但Shepherd仍有很大的成长空间! 例如,共享迁移仍然有些乏味-我们认为能够为Shepherd提供npm软件包或指向要旨的链接并自动获取迁移是很酷的。 Shepherd也相对较慢,因为在每个存储库中顺序执行更改。 能够在一台机器上甚至在多台机器上并行化多个存储库真是太棒了。 查看我们的一些GitHub问题,以了解我们想到的其他一些想法。
如果您想了解更多有关Shepherd工作原理的细节,或者想通过一个更完整的示例,请查看GitHub上的教程。 如果您有任何问题或意见,请随时通过意见或GitHub问题与我们联系!