自2019年发布GitHub Actions以来,GitHub一直在大力改进CI/CD体验。作为这项投资的一部分,可重复的任务可以作为自定义操作提供,并在社区外部或GitHub Enterpriseinstance内部共享。
在Blend,我们从采用GitHub操作中受益匪浅。我们已经构建了能够在Go中编写动作的工具,并自动化了GitHub企业实例中动作的发布过程。下面,我们将分享运行和发布用GOA编写的操作的一系列独特挑战,并概述我们解决这些问题的策略。
GitHub发布了大量教程和许多基础动作1作为示例。为了让社区使用相同的工具,他们在npm上发布了@actions/core包。
action是带有根操作的GitHub存储库。yml文件和支持文件。对于动作的作者来说,最常见的选择是用JavaScript编写动作,并在工作流期间在动作运行程序上以代码方式运行。如果这不是选项2,Docker容器操作允许运行Docker映像;图像可以直接从Docker文件构建,也可以从公共Docker注册表获取。
第三个选项是复合操作,它允许将操作创建为一系列步骤(非常类似于GitHub操作工作流中作业的工作方式)。这使得编写轻量级操作成为可能,例如使用shell脚本。复合动作甚至可以引用这些步骤中的其他动作。
为了分发用Go编写的操作,我们从源代码中构建静态二进制文件,并将它们签入GitHub存储库中以供操作。
对于需要在运行Linux或Windows的64位x86和ARM机器上运行的操作,使用六个文件就足够了:
$tree。├── 行动yml├── 调用二进制文件。js├── main-linux-amd64-e9d351bd367300ec85b9ba777812c42be2570a64├── main-linux-arm64-e9d351bd367300ec85b9ba777812c42be2570a64├── main-windows-amd64-e9d351bd367300ec85b9ba777812c42be2570a64└── main-windows-arm64-e9d351bd367300ec85b9ba777812c42be2570a640目录,6个文件
为了调用正确的静态二进制文件,我们使用一个小小的JavaScript垫片来确定当前的操作系统(GOOS)和平台/架构(GOARCH)。垫片发送正确的静态二进制文件,如下所示:
函数选择binary(){/…if(平台==';linux';&;&;&;arch===';x64';){return`main-linux-amd64-${VERSION}`/..}const binary=chooseBinary()const mainScript=`${_dirname}/${binary}`const spawnsynchreturns=childProcess。spawnSync(主脚本,{stdio:';继承';})
在行动中。yml我们只是“假装是JavaScript”来呼唤我们的shim:
在Blend,我们在内部Go monorepo中保留所有Go操作。我们将其发布到GitHub Enterpriseinstance内的actions组织中。更新Go操作时,合并后的步骤会为我们需要支持的体系结构子集构建静态二进制文件,并将提交直接推送到相应的actions/${action}存储库。例如,将Go monorepo更改为project/github actions/{cmd,pkg}/build docker image/中的代码将导致提交到actions/build docker image存储库。
使用这种方法,用Go编写的操作会立即运行,就像用JavaScript编写的操作一样。我们消除了对actions runner的任何Go依赖:GitHub只获取我们的调用二进制文件。js和静态二进制文件。为了加快检索速度,我们特意压缩静态二进制文件,并创建一个特殊的“发布”分支,其中包含运行Action 3所需的最小文件集。
我们使用预构建二进制文件的方法与推荐的JavaScript操作方法在精神上是相同的。对于JavaScript操作,建议使用ncc编译器创建单个索引。jsfile。有了这个单文件入口点,操作就可以执行该文件,而无需任何其他必要的设置。由于Go是一种编译语言,所以并没有“我有一些源代码和解释程序”节点的直接等价物。js方法,因此需要包含预构建的二进制文件。有趣的是,ncc项目将Go编译器列为其动机之一,所以肯定有什么原因!
这种方法绝对可以被编程语言生态系统而不是Go使用。例如,使用针对Python的Nuitka编译器,可以以相同的方式生成StandaloneExecutable。对于C++语言这样的编译语言,创建预构建的二进制文件是很简单的。
Go在这个领域的一个独特优势是创建静态链接二进制文件的默认模式。这使得只在一台新机器上运行而不需要安装或定位依赖项变得更加容易。此外,Go编译器从单个开发机器交叉编译二进制文件的能力对于我们在这里使用的分发策略非常有用:
通常,我们尝试将cmd/${ACTION}/main设置为。go脚本尽可能短,这样我们可以最大限度地增加可测试的代码量5:
//文件:cmd/impositional/main。go软件包主要导入(";context";githubactions";github.com/sethvargo/go githubactions";";github.com/blend/impositional action/pkg/impositional";)func run()错误{ctx:=context.Background()操作:=githubactions.New()cfg,err:=NewFromInputs(操作),如果err!=nil{return err}返回假设。Run(ctx,cfg)}func main(){err:=Run(),如果err!=nil{action.Fatalf(";%v";,err)}
通过从一开始就加载所有输入和配置,可以更容易地对操作进行推理:解析后,可以将单个configurationstruct传递给实现业务逻辑的代码。例如:
//文件:pkg/impositional/config。go type Config struct{Role string LeaseDuration time.Duration}func NewFromInputs(action*githubactions.action)(*Config,error){lease:=action.GetInput(";lease Duration";)d,err:=时间。如果出现错误,解析持续时间(租约)=nil{return nil,err}c:=Config{Role:action.GetInput(";Role";),租赁:d、}退货&;c,无}
sethvargo/go githubactions项目提供了一个与@actions/core JavaScript包大致相当的惯用软件包。我们尽可能地利用它,但尝试遵循一些更大的原则来编写可测试代码。
在编写使用githubactions包的代码时,指针操作*githubactions。应该使用Action,而不是包defaultAction结构周围的全局包装器。例如:
为了测试涉及GitHub操作的代码,关键是既要能够控制环境变量(这些是输入),又要能够监视对输出的写入。为了在测试中做到这一点,可以替换标准输出编写器和Getenv()提供程序:
func TestNewFromInputs(t*testing.t){/…actionLog:=bytes.NewBuffer(nil)envMap:=map[string]string{";INPUT#u ROLE";:";user";";INPUT#LEASE-DURATION";:";1h";}getenv:=func(key string)string{return envMap[key]=hubActions:=。新的(githubactions.WithWriter(actionLog),githubactions。使用getenv(getenv),)/。。。信息技术相等(";…";,actionLog.String())}
为了检查实现的合理性,可以在本地运行操作,而不是执行预发布并等待完全触发的GitHub操作工作流。要在本地运行操作,使用正确的环境变量运行Go脚本就足够了。
需要两种类型的环境变量。第一类是工作流附带的GITHUB_*环境变量。另一种类型是输入中提供的输入:操作(即action.yml的输入),GitHub将其转换为输入*环境变量。
使用预构建的静态二进制文件并不是编写操作的唯一选择。我们明确考虑使用Docker容器操作或复合操作,但选择不使用这两种操作。
这里第一个明显的选择是为Gocode编写Dockerfile并使用Docker容器操作。例如,GopHeCub 2021的GITHUB动作教程推荐了这种方法,就像GithuBACTS包的发布说明一样。然而,这种体验并不像JavaScript体验那样流畅。
通过Docker容器操作,图像引用可以是DockerFile或容器注册表中的图像。引用Dockerfile会直接带来巨大的成本:每调用7次操作,就需要构建映像。从公共容器注册表中提取图像模拟JavaScript操作的快速“立即运行”行为。对于存储在私有容器注册表中的图像,这会产生一个新的挑战。要使用引用私有映像的操作,用户需要首先对注册表进行身份验证,以获取他们甚至不知道正在使用的映像:
步骤:-名称:登录到ECR使用:docker/登录-action@v1使用:注册表:${env.AWS_ACCOUNT_NUMBER}。dkr。ecr${{env.AWS_地区}。亚马逊。com用户名:${secrets.AWS_-ACCESS_-KEY_-ID}}密码:${{secrets.AWS_-SECRET_-ACCESS_-KEY}名称:调用假设用法:混合/假设-action@main使用:角色:用户租赁期限:1小时
对于工程组织(例如GitHub企业实例)中使用的内部操作,大多数图像很可能位于私有容器注册表中。
通过使用复合操作,可以避免Docker构建和身份验证的开销。从Go源代码8开始,唯一的选择是编译并运行该代码。这意味着复合操作需要确保Go安装在操作运行程序上。例如:
运行:使用:复合步骤:-使用:操作/设置-go@v2使用:go版本:';1.17.6' - 快跑:快跑/主要的去
然而,这与在运行DockerFile之前构建DockerFile的方法存在相同的问题:默认使用(无缓存)会导致在操作实际运行之前等待构建的巨大成本。此外,在actions runner上运行actions/setup go可能会覆盖由实际调用此操作的工作流作业安装的现有版本的go,从而导致使用此操作的工作流中出现不可见的中断。
使用一个小小的JavaScript垫片,用Go编写的动作可以与本机JavaScript动作保持一致。正如我们前面所讨论的,这种一流的本地支持带来了许多好处,主要是调用速度和设置的简单性。最重要的是,这让我们在编写动作时能够充分利用围棋的所有好处。对于工程组织中的内部操作,这允许我们在操作中重用现有的Go库。有了GitHub Actions和Go,我们就可以吃蛋糕了。
例如,使用actions/checkout签出代码,使用actions/cache-to-cache依赖项加快CI运行。↩︎
这意味着我们不需要在“release”分支中包含任何Go源代码(或Go.mod等)。出于同样的原因,我们不想将不完整的静态二进制文件检查到“开发”分支。↩︎
值得一提的是:cgo不是Go。使用cgo生成静态二进制文件是可能的,但更具挑战性。交叉编译无定向二进制文件更具挑战性。↩︎
虽然可以在包main中测试代码,但这并不常见。对于以os结尾的代码路径来说,这尤其具有挑战性。退出()。↩︎
对于烤肉串案例中的操作输入,相应的环境变量将有一个连字符,例如INPUT_LEASE-DURATION。在大多数shell中,使用带有连字符的environmentvariable名称需要格外小心,因此使用env INPUT_LEASE-DURATION=。。。。↩︎
当然可以使用actions/cache跨工作流运行使用Docker构建上下文,但要正确设置并不容易。这给操作的用户带来了不必要的负担。↩︎
“从Go源代码开始”,而不是另一种方法,即只分发预构建的二进制文件。↩︎