解锁新遗物红宝石代理的秘密权力,我分享了如何从Ruby应用程序获得更丰富的遥测数据。对于任何技术,特别是开源项目,我们都在不断改进我们的Ruby代理。这意味着我将带着更多关于如何解锁我们在努力中发现的更多秘密力量的建议回来。

沿途来开设Ruby代理,我们从内部托管的Travis CI实施中移动了我们的持续集成(CI)GitHub的行为.我们学到了很多关于GitHub Actions的知识,如果让这些知识隐藏在我们的知识库的阴影中,那将是一种耻辱。虽然以红宝石为主题,但这篇文章并没有充满红宝石的花边新闻。相反,我将向Ruby开发人员展示如何走出他们的舒适区,进入JavaScript领域,以便他们能够构建可靠的持续集成工作流。

New Relic Ruby代理的持续集成工作流

首先,让我们建立一些我们在Ruby代理团队中完成的基础工作。作为我们在开源领域投入更多精力的一部分,我们构建了一个持续集成工作流,可以可靠地构建从版本2.0到当前版本的所有二进制Ruby解释器,并针对每个版本运行我们的测试套件。我们的测试矩阵扩展到150多个工作岗位。这是大量的工作,我们需要它有效地运行,以保持总体构建和测试时间在一个合理的水平上。事实上,看看我们的All Things Open 2020 Presentation了解更多我们取得的成果。我们将测试性能/效率提高了577%,这是一个不小的成就。下面的技巧涵盖了没有很好地文档化的主题,或者对非javascript专家开发人员来说可能不是很明显的主题。

GitHub建议,动作应该是单一目的、可重用和可共享的。然而,也存在一些大型的复杂操作,如果它们编写得很好,它们可以通过删除YAML文件中脆弱且难以维护的重复代码来极大地增强您的工作流。不应该仅仅因为GitHub设想的设计目标而回避一种方法。

开始与一个基于javascript的GitHub动作脚本

在一个复杂的工作流程中,最大的挑战是获得一个完整的解决方案组装和工作。在这种情况下,我不能强调采取最简单的方法:先构建,然后提取。

您可能很想遵循一个模板或一篇博客文章来说明如何为教程和演示动作脚本设置单独的存储库。对于您的第一个动作脚本,这是一个不必要的负担。除非您知道您正在计划发布和共享您将长期维护的解决方案,否则设置新存储库、记录它、构建测试套件等额外的步骤只会妨碍简单地让事情工作。将操作放在主项目存储库中是完全可以的。

在这个例子中,你将遵循GitHub的惯例,将与GitHub相关的东西保存在项目的根文件夹中:~ / .github文件夹中。你构建的每个动作脚本都应该这样命名:~ / .github /行动/ <动作名称>.一个本地托管的操作通过来自项目根目录的相对路径在工作流文件中被引用:

- name: Build Ruby ${{matrix.ruby-version}}使用:./。Github /action /build-ruby with:

Github的行为要么用纯JavaScript实现,要么用TypeScript(提供面向对象特性)实现。您需要在这些选项之间进行谨慎的选择,因为设置操作运行环境的大部分工作取决于您对构建环境的选择。出于我们的目的,让我们坚持使用纯JavaScript。

有很多关于安装JavaScript构建环境的详细指南,但是在所有情况下,您都需要安装NodeJS对于JavaScript运行时环境,用于依赖项管理,以及ncc对于操作脚本的编译器/汇编程序。如果您在MACOS上,请运行以下命令:

Brew install node Brew install yarn I -g @vercel/ncc

下一步,将两个条目添加到“脚本”Section将所有内容编译为动作的~ / dist文件夹:

{“name”:“您的动作名称”,“版本”:“1.0.0”,“描述”:“短操作说明”,“main”:“index.js”,“脚本”:{“LINT“:”eslint * .js“,”包“:”ncc build index.js -o dist“},...

现在安装GitHub操作工具包组件,让您点击构建操作脚本所需的好东西:

npm install @actions/core # for interactive with Github action cache # for interactive with Github action cache # for interactive with Github action core function npm install @actions/exec # for interactive with Github action core function

在动作的根文件夹中创建一个index.js文件

要正确地将您的操作与Node的并发模型和GitHub Actions集成在一起,您需要密切关注如何定义您的方法和脚本的初始入口。下面是一个很好的开始:

Const OS = Quanc('OS')const = exiple('fs')const path = require('path')const crypto = require('crypto')const core = feed('@动作/核心')const exec=要求('@ actions / exec')const cache = require('@操作/ cache')const io = require('@操作/ io')async函数dosomingusful(){core.startgroup(`做一些有用的东西)等待睡眠(1)core.endgroup()} async函数main(){try {await dosometingsful()} catch(错误){core.setfailed(`setsefeed fear forr $ {error }`)} main()

正如你在上面看到的主要入口函数定义为异步,因此它将以非阻塞、并发的方式运行。对于大多数行动,你都希望等待行动中的具体步骤来完成。通常还会将action中的每个新函数/步骤声明为异步函数。使用等待有效地阻止直到你的步骤完成。这个模式开始于主要进入函数并在整个脚本中持续。

一个最终设置组件:预先提交的钩子

因为一旦部署,所有东西都需要编译/组装才能运行,所以很容易忘记构建和签入index.js.预提交钩子确保动作脚本的最新版本总是被检入:

#!/bin/sh设置-e CD .github/actions/your-action-script yarn运行package exec git add dist/index.js

我发现将这个预提交脚本作为文件保存在动作的根文件夹中是一种很好的做法。在README中,提供在本地激活预提交的说明,以便所有贡献者发现和安装。

使用Action Scripts的提示和技巧

动作脚本中的缓存

缓存在工作流文件中有很好的记录,而且在动作脚本中做同样的事情比您想象的要容易。在动作脚本中控制缓存,而不是在工作流中,可以让您更细粒度地控制何时恢复和何时缓存内容。我们还发现,在动作脚本中管理缓存非常重要干就因为我们有多个步骤,每个步骤都需要声明和恢复缓存的ruby二进制文件。

下面是保存和恢复动作脚本构建的Ruby二进制文件的方法:

function rubyCachePaths(rubyVersion) {return [' ${process.env.HOME}/. rubyVersion /ruby-${rubyVersion} ']}//如果存在,将尝试恢复以前构建的Ruby环境。async函数restorubyfromcach(rubyversion){core.startgroup(`从cache ruby​​ ruby​​)const key = ruby​​cachekey(rubyversion)等待cache.restorecache(rubycachepaths(rubyversion),key,[key])core.endgroup()}//导致当前的Ruby环境归档和缓存。async function saveRubyToCache(rubyVersion) {core。startGroup(' Save Ruby to Cache ') const key = rubyCacheKey(rubyVersion) await Cache . savecache (rubyCachePaths(rubyVersion), key) core.endGroup()}

rubyversion.来自工作流文件作为矩阵的一部分,我们提取并通过主要脚本中的入口点。我们选择缓存而不是构建和发布工件,因为这些Ruby二进制文件并不真正用于公共消费,这将意味着设置可能被其他人使用的单独构建存储库。

文件指纹的散列函数

我们看到的大多数保存和恢复Ruby绑定宝石的操作都依赖于来自gemfile.lock.lock..因为我们自己发布了一个gem,所以我们没有签入gemfile.lock.lock.,按照编写Ruby gems的最佳实践。这意味着我们需要另一种方法来建立指纹。我们选择了采集宝石的指纹.gemspec文件。诀窍是发现如何同步读取和哈希该文件。这是我们到达的解决方案:

//指定文件名,返回十六进制字符串表示函数fileHash(文件名){让之和= crypto.createHash (md5) sum.update (fs.readFileSync(文件名))返回sum.digest(十六进制)}函数bundleCacheKey (rubyVersion) {const keyHash = fileHash ($ {process.env.GITHUB_WORKSPACE} / newrelic_rpm.gemspec)返回v2-bundle-cache - {rubyVersion} - {keyHash}’美元}

这就是我加入的原因'crypto'“fs”在上面的例子中。的“fs”模块给了我们访问权限,所以我们可以同步读取文件的内容(即阻塞I/O)和'crypto'模块提供了在文件内容上生成MD5摘要指纹的方法。

编写实用函数——它们将使您的生活更轻松

当我们建立了行动脚本时,我们发现写小实用功能使我们的生活更容易。因为我们在日常生活中不使用JavaScript,所以这些小函数提供了一种避免引入bug的安全方法。以下是我们如何始终如一地预先添加环境变量的示例:

//将给定的值添加到环境变量function prependEnv(envName, envValue, divider=' ') {let existingValue = process.env[envName];if (existingValue) {envValue += ' ${divider}${existingValue} '} core;exportVariable (envName envValue);}//专门针对EOL的红宝石所需的任何设置async function setupOldRubyEnvironments(rubyVersion) {core. txt;const openSslPath = rubyOpenSslPath(rubyVersion);核心。exportVariable('OPENSSL_DIR', openSslPath) prependEnv('LDFLAGS', ' -L${openSslPath}/lib ') prependEnv('CPPFLAGS', ' -I${openSslPath}/include ') prependEnv('PKG_CONFIG_PATH', ' ${openSslPath}/lib/pkgconfig ', ':') core.endGroup()}

并行下载;安装顺序

在下载大文件时,我们利用了Node的并发性。我们使用并行下载,但串行运行安装,因此我们不必与系统管理器锁竞争条件作斗争。下面的示例展示了如何开始Javascript的承诺并解决它们:

//旧的ruby也需要旧的MySQL,它是基于旧的OpenSSL库构建的。//否则mysql适配器将在Ruby中分段错误,因为它尝试动态链接//到1.1系列,而Ruby链接对1.0系列。async函数下降ademysql(){core.startgroup(`downgrade mysql`)const pkgdir =`$ {process.env.home} / packages` const pkgoption =`--directory-prefix = $ {pkgdir} /`const mirorurll ='https://mirrors.mediatemple.net/debian-security/pool/updates/main/m/mysql-5.5'//并行执行以下内容const promise1 = exec.exec('sudo',['apt-get','remove','mysql-client'])const promise8 = exec.exec('wget',[pkgoption,`$ {mirrorurl} / libmysqlclient18_5.5.62-0%2bdeb8u1_amd64.deb`])const promise3 = exec.exec('wget',[pkgoption,`$ {mirrorurl} / libmysqllient-dev_5.5.62-0%2bdeb8u1_amd64.deb`)//等待并行进程完成等待的承诺。所有([promise1、promise2 promise3])/ /执行连续等待执行。exc.('sudo', ['dpkg', '-i', `${pkgDir}/libmysqlclient18_5.5.62-0+deb8u1_amd64.deb`]) await exec.exec('sudo', ['dpkg', '-i', `${pkgDir}/libmysqlclient-dev_5.5.62-0+deb8u1_amd64.deb`]) core.endGroup() }

获取shell命令的输出

与我使用过的大多数语言不同,在JavaScript中运行shell命令会返回退出代码,而不是发出的输出STDOUT.此外,exc.函数以Promise的形式运行,所以它也是非阻塞的。注意这两个问题,这样你就不会发现后续步骤失败了。如果你想运行一个shell命令并捕获它的输出,连接回调监听器来捕获输出,如下所示:

//调用带有监听器的@actions/exec exec函数来捕获//输出流作为返回结果。async函数执行(命令){try {let outputstr =''const选项= {} options.listeners = {stdout :( data)=> {outputstr + = data.tostring()},stderr :(数据)=> {Core.Error(data.tostring())}} await exec.exec(命令,[],选项)返回outputstr;catch(错误){console.Error(Error.Tostring())}}

使用树检查您的环境设置

当您在不熟悉的领域时,可能很难弄清楚在哪里安装东西,在哪里执行东西。在这种情况下,我们使用Linux tree命令来查看在哪里安装了什么。您可以使用在动作脚本和工作流中使用。使用在一个Ubuntu运行器上是如此简单:

运行:| sudo apt-get install tree tree。

pièce de résistance:注释

当超过150个运行作业出现失败时,您最不希望做的事情就是导航到每个作业中,查看失败的确切原因。GitHub中的注释提供了一种以摘要形式查看信息的方式。然而,这并不是一个记录良好的特性。即使调试一个或两个作业失败,也可能是一项主要工作。例如:

我们来解决这个问题。我们可以捕获失败作业的输出并编写消息,注释将捕获信息并格式化并在注释上面所示的部分。

幸运的是,我们有一个自产的测试框架,叫做Multiverse。这个工具类似于评估以及其他Ruby gems,它们将在Ruby二进制文件和绑定gems的不同组合下运行测试套件。Multiverse捕获所有测试运行的所有输出,我们只是扩展了现有的功能,将所述输出写入error.txt输出文件。实际上,我们的Multiverse测试套件运行并生成输出(通常写入控制台),现在我们还将该输出写入error.txt检测到失败套件时的文件。这是我们如何做到的:

#将失败的输出保存到容器的工作目录在github工作流的注释中读取和输出def self.save_output_to_error_file(线)#是因为各种环境可能以单独的线程运行到#启动他们的进程,确保我们不完全交织输出。@ output_lock.synchronize do filepath = env [“github_workspace”output_file = file.join(filepath,“errors.txt”)现有_lines = []如果file.exist?(output_file)现有_lines + = file.read(output_file).split(“\ n”)结束行=行。如果线条(字符串)file.open(output_file,'w')do | f | f |f.puts现有_lines f.puts“*”* 80 f.puts线条结束结束结束

@output_lock互斥防止并发作业写入混合输出。现在,回到动作脚本中,我们寻找这个errors.txt文件并将任何此类文件内容转录到如下所示的注释字段:

const fs = require('fs') const core = require('@actions/core') const command = require('@actions/core/lib/command')GITHUB_WORKSPACE const errorFilename = ' ${workspacePath}/errors.txt '尝试{if (fs.existsSync(errorFilename)) {let lines = fs.readFileSync(errorFilename). tostring ('utf8')命令。issueCommand('error', undefined, lines)} else {core.info('没有${errorFilename}存在。')}} catch (error) {core。setFailed(' Action failed with error ${error} ')}} main()

请注意,由于注释与构建Ruby二进制文件无关,所以我们将注释步骤作为其自身包含的Javascript操作来编写。此操作需要访问“fs”(文件系统),'核',“命令”在这'核'库以写入注释字段。如果存在并呼叫,所需的所有此操作脚本都需要读取错误.TXT文件的内容并调用“issueCommand”“错误”标记和文件中的行。

最终的结果是这样的:

现在我们可以一看是在许多工作中发生了普遍的失败,还是每个工作都有其独特的失败点。我们不再需要深入研究每个作业,然后深入到数千行输出,以找出每个失败作业的确切错误。

因为注释是一个单独的动作,我们必须根据需要在工作流中调用它。在我们可能有错误的每个步骤结束时,我们这样做:

- name:注释错误,如果:${{failure()}}使用:./.github/actions/ Annotate

闭幕词

GitHub Actions是一个强大的解决方案,可以自动化你的工作流中几乎任何事情。虽然我们的第一反应是避免使用JavaScript,因为我们不是JavaScript专家,但我们很快就学会了在GitHub提供的环境中使用它。有一个丰富的生态系统利用JavaScript,因为它是GitHub在构建他们的工具包时使用的框架。希望以上的提示和技巧能给你一个跑步的开始,让你不断努力。

要了解有关Ruby代理和其他开源贡献的更多信息,请查看我们所有的项目New Relic开源

Michael是一位精通IT的专业人士,在软件开发和硬件系统管理方面都有很深的根基。在New Relic,他带领Ruby Agent团队完成了他们的代理开源之旅。他精通Ruby生态系统的所有领域,包括Rails框架、选择要使用的gem(库)、托管解决方案、自动化测试和部署,以及解决性能和可伸缩性问题。亚博直播平台查看贴子

有兴趣为New Relic博客写作吗?亚搏体育登入网给我们一个推介