解锁新遗物红宝石代理的秘密权力,我分享了如何从Ruby应用程序获得更丰富的遥测数据。与任何技术一样,特别是开源项目,我们不断改进我们的Ruby代理。这意味着我回到了更多关于如何解锁我们在努力期间发现的秘密权力的提示。

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

新的遗物红宝石代理的持续集成工作流程

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

GitHub建议,行动是单一的,可重复使用和可共享的。但是,大型复杂操作也存在,并且如果它们写得很好,它们可以通过删除脆弱的代码来大大提升您的工作流,这些代码在yaml文件中维护。仅仅因为GitHub设想的设计目标,不应该在另一个方面避开一种方法。

基于JavaScript的GitHub操作脚本入门

在一个复杂的工作流程中,最大的挑战是组装并运行一个完整的解决方案。在这种情况下,我再怎么强调采用最简单的方法也不过分:先构建,然后提取。

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

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

—name: Build Ruby ${{matrix.ruby-version}}使用:./。github/actions/build-ruby with: ${{matrix.ruby-version}}

github动作以纯JavaScript或TypeScript(提供面向对象的特性)的方式实现。您需要在这些选项中谨慎选择,因为设置操作运行环境的大部分工作取决于您对构建环境的选择。出于我们的目的,让我们继续使用纯JavaScript。

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

brew install node brew install yarn i -g @vercel/ncc

接下来,将这两个条目添加到“脚本”部分将所有内容编译到操作中~ / dist夹:

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

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

NPM Install @操作/缓存#用于与GitHub操作缓存NPM Install @操作/核心#用于与Github操作核心功能NPM Install @操作/ exec#用于从JavaScript NPM Install @操作/ io#for文件i/ o操作(如文件副本)

在操作的根文件夹中创建index.js文件

要将操作与节点的并发模型和GitHub操作正确集成,您将需要密切关注您如何定义您的方法和初始条目到脚本中。以下是开始的好方法:

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中的每个新函数/步骤声明为异步功能。采用等待有效地阻止,直到您的步骤运行完成。这个模式从the开始主要进入函数并在整个脚本中持续。

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

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

# ! / bin / shset -e cd .github/actions/your-action-script yarn run package exec git add dist/index.js

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

使用Action Scripts的提示和技巧

动作脚本中的缓存

缓存在工作流文件中被良好地记录,它比你可能认为在动作脚本中执行相同的更容易。控制动作脚本中的缓存而不是工作流程,为您提供更细粒度的控制,何时还原以及何时缓存内容。我们还发现,显着管理动作脚本中的缓存干就自从我们有多个步骤以来,我们的工作流文件,以及所需的每一步都需要声明和恢复缓存的Ruby二进制文件。

下面是如何保存和恢复我们的动作脚本构建的Ruby二进制文件:

${rubyVersion} / ${rubyVersion} / ${rubyVersion} / ${rubyVersion} / ${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函数saverubytocache(rubyversion){core.startgroup(`save ruby​​到cache`)const键= ruby​​cachekey(rubyversion)等待Cache.saveCache(rubycachepaths(rubyversion),key)core.endgroup()}

rubyversion.作为矩阵的一部分,从工作流文件中提取并传递主要脚本中的入口点。我们选择缓存而不是构建和发布工件,因为这些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=' '){让existingValue = process.env[envName];if (existingValue) {envValue += ' ${divider}${existingValue} '} core. if (existingValue) {envValue += ' core. 'exportVariable (envName envValue);}//专门针对EOL的红宝石所需的任何设置async函数setupoldrubyenvironments(rubyversion){core.startgroup(“eol ruby​​环境的设置”)const opensslpath = ruby​​opensslpath(rubyversion);core.portVariable('openssl_dir',opensslpath)prependenv('ldflags',`-l $ {opensslpath} / lib`)prependenv('cppflags',`-i $ {opensslpath} / compnete`)prependenv('pkg_config_path',`$ {opensslpath} / lib / pkgconfig`,':')core.endgroup()}

并行下载;连续安装

下载大文件时,我们利用了Node的并发性。我们使用并行下载,但以串行方式运行安装,因此不必应对系统管理器锁定竞争条件。下面的示例演示如何启动JavaScript承诺并解决它们:

//旧的ruby也需要旧的MySQL,它是基于旧的OpenSSL库构建的。//否则mysql适配器将在Ruby中出现segfault,因为它试图动态链接//到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.函数作为承诺运行,因此它也不阻止。请注意这两个问题,因此您不会发现您的操作中的稍后步骤失败。如果要运行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())}}

使用tree检查环境设置

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

—名称:show me the tree运行:| sudo apt-get install tree。

PièceRésistance:诠释

当你遇到超过150个正在运行的作业失败时,你最不想做的事情就是进入到每个不同的作业中去查看失败的确切原因。GitHub上的注释提供了一种以摘要形式查看信息的方式。然而,这并不是一个有充分文档证明的特性。调试即使是一两个作业失败也可能是一项繁重的工作。例如:

所以,让我们解决这个问题。我们可以捕获失败作业的输出,并写下注释将抓取信息和格式的消息并显示注释上面所示的部分。

幸运的是,我们有一个自己开发的测试框架,叫做Multiverse。这个工具类似于评价以及其他Ruby gems,它们将在不同的Ruby二进制文件和捆绑gem组合下运行测试套件。Multiverse捕获所有测试运行的所有输出,我们简单地扩展了现有的功能,将所述输出写入错误.txt.输出文件。实际上,我们的Multiverse测试套件运行并生成一个输出(通常写入控制台),现在我们还将该输出写入错误.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 = excile('fs')const rcor = require('@ actions / core')const命令= require('@ action / core / lib / command')async函数main(){const workspacepath = process.env。github_workspace conster errorfilename =`$ {workspacepath} / errors.txt`尝试{if(fs.existssync(errorfilename)){let行= fs.readfilesync(errorfilename).tostring('utf8')command.issuecommand('错误',undefined,行)} else {core.info(`no $ {errorfilename}存在。跳过!`)}} catch(错误){core.setfailed(`setfefailed(`操作失败,错误$ {error }`)} main()

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

最终的结果是这样的:

现在我们一眼就能看出,是很多工作都出现了普遍的失败,还是每个工作都有自己独特的失败点。我们不再需要深入到每个作业中,然后再深入到数千行输出中,以确定每个失败的作业到底出了什么问题。

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

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

闭幕词

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

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

Michael是一位有成就的IT专业人士,在软件开发和硬件系统管理方面都有很深的经验。在New Relic,他带领Ruby特工团队开启了他们的特工开源之旅。他在Ruby生态系统的所有领域都非常熟练,包括Rails框架、使用gems(库)的选择、托管解决方案、自动化测试和部署,以及解决性能和可伸缩性问题。188bet亚洲体育查看贴子

有兴趣为New Relic博客写作吗?188博彩体育网址给我们发一份建议书!!