如何在GitHub Actions中缓存步骤?

82

假设我有一个包含2个步骤的 GitHub actions 工作流程。

  1. 下载并编译我的应用程序依赖项。
  2. 编译和测试我的应用程序。

我的依赖关系很少更改,已编译的依赖关系可以安全地缓存,直到下次更改锁定文件以指定它们的版本。

有没有一种方法可以保存第一步的结果,以便将来的工作流程可以跳过该步骤?


1
尽管bitoiu的回答是正确的,即GitHub Actions今天没有显式的缓存功能,但在给定的工作流运行中,您可以跨步骤获得隐式缓存。这是因为GitHub将您的repo挂载到Docker中的每个步骤中。您在一个步骤中所做的任何更改都会持久保存到该工作流运行的下一个步骤中。当然,这不会缓存依赖项构建以跨运行使用,但其他人可能会发现具有“某些”缓存功能很有用。我认为这个功能没有文件记录。 - Taylor D. Edmiston
1
你还可以尝试的另一件事是,像 GitLab 的分布式缓存系统一样,将构建依赖缓存(例如 tar 包)推送到 S3/Minio/等等。目前,你需要手动进行与 S3 或类似服务的请求传输,直到 GitHub Actions 产品添加此类功能。这种方法能为你节省多少时间(如果有的话),当然取决于你的依赖项大小以及 GitHub Actions 从 S3 提取的速度。我自己还没有测试过这个方法。 - Taylor D. Edmiston
GitHub将删除任何未被访问超过7天的缓存条目。引自GitHub文档:https://docs.github.com/en/actions/guides/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy - 0 _
6个回答

61

大多数用例都可以使用现有的操作来完成,例如:


可以通过 缓存操作 来支持自定义缓存。它适用于存储库中的作业和工作流。另请参阅:GitHub文档示例

考虑 以下示例

name: GitHub Actions Workflow with NPM cache

on: push

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - name: Cache NPM dependencies
      uses: actions/cache@v3
      with:
        path: ~/.npm
        key: ${{ runner.OS }}-npm-cache-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          ${{ runner.OS }}-npm-cache-

    - name: Install NPM dependencies
      run: npm install

缓存是如何逐步实现的:

  • Cache NPM dependencies步骤中,操作将检查当前key是否存在现有缓存
  • 如果没有找到缓存,它将使用restore-keys检查空间匹配项。在这种情况下,如果package-lock.json更改,则会返回以前的缓存。将键和恢复键与缓存的 OS 和名称前缀一起使用很有用,因为它不应该为不同类型的缓存或操作系统加载文件。
  • 如果找到任何缓存,则会将文件加载到path
  • CI继续下一步并可以使用从缓存中加载的文件。在这种情况下,npm install将使用~/.npm中的文件来节省网络下载(请注意,对于NPM,直接缓存node_modules不是推荐的方法)。
  • 在CI运行结束时,将执行后置操作以保存更新的缓存,以防key发生更改。这在工作流程中没有明确定义,而是内置到缓存操作中,以同时处理缓存的加载和保存。

您还可以使用@actions/cache构建自己的可重用缓存逻辑,例如:


旧答案:

目前不支持本地缓存,预计在2019年11月中旬实施

您可以使用构件(12)按照 GH社区版 上的建议在作业之间移动目录(在一个工作流内)。然而,这种方法不能在工作流之间运行


新答案:

旧答案:

目前不支持本地缓存,预计在2019年11月中旬实施。

您可以使用构件(1,2)按照 GH社区版 上的建议在作业之间移动目录(在一个工作流内)。然而,这种方法不能在工作流之间运行。


8
如果不包含 if: steps.cache-deps.outputs.cache-hit != 'true'id: cache-deps 这样的条件语句和缓存步骤,这个例子是否仍然会在每次推送时运行 npm install 步骤? - Janosh
2
@thisismydesign,如果我仍然需要等待npm install步骤,从缓存中加载依赖项的好处是什么? - DannyFeliz
3
提醒一下不知情的人,这会远程与 GitHub 交换缓存。因此,如果您正在使用本地管理的 GitHub 执行程序(顺便说一下,这很棒),则需要承担上传和下载这些缓存所需的时间和成本。 - Joshua Pinter
3
这个回答实际上并没有解释如何在随后的作业中使用缓存,这正是@action/cache的全部意义。它只是重复了GitHub无意义的文档,而这份文档也没有解释如何消耗缓存。 - Steve
4
在文档中没有明确提到的一点是,当缓存搜索未命中时,缓存操作会评估路径以便于在作业结束时随后进行存储。我对于为什么每个示例都没有两个缓存操作的使用感到困惑(一个用于尝试恢复,另一个则在生成文件后仅在缓存未命中时进行存储),因为文档让人感觉缓存操作是在使用时存储,而不是在末尾进行。虽然通过缓存仅在作业成功时才存储文件有些含蓄地表达了这一点,但它可能需要更加明确。 - oblivioncth
显示剩余5条评论

15

cache操作仅能缓存文件夹的内容。如果有这样的文件夹,您可以通过缓存它来节省一些时间。

例如,如果您使用某个虚构的package-installer(例如Python的pipvirtualenv,或NodeJS的npm,或其他将其文件放入文件夹中的任何工具),您可以通过以下方式来节省一些时间:

    - uses: actions/cache@v2
      id: cache-packages  # give it a name for checking the cache hit-or-not
      with:
        path: ./packages/  # what we cache: the folder
        key: ${{ runner.os }}-packages-${{ hashFiles('**/packages*.txt') }}
        restore-keys: |
          ${{ runner.os }}-packages-
    - run: package-installer packages.txt
      if: steps.cache-packages.outputs.cache-hit != 'true'

那么这里的重点是:

  1. 我们给这个步骤起一个名字,cache-packages
  2. 稍后,我们将使用此名称进行条件执行:ifsteps.cache-packages.outputs.cache-hit != 'true'
  3. 为缓存操作指定要缓存的文件夹的路径:./packages/
  4. 缓存键:它取决于输入文件的哈希值。也就是说,如果任何packages.txt文件更改,则会重新构建缓存。
  5. 第二步,包安装程序,仅在没有缓存时运行

对于virtualenv用户:如果您需要激活某些shell环境,则必须在每个步骤中执行此操作。像这样:

- run: . ./environment/activate && command

4

4
我的依赖关系很少改变,编译的依赖项可以安全地缓存,直到我下次更改指定它们版本的锁定文件。有没有一种方法可以保存第一步的结果,以便将来工作流程可以跳过该步骤?
第一步是:
下载并编译我的应用程序的依赖项。
GitHub Actions 本身不会为您执行此操作。我唯一能给您的建议是遵循 Docker 最佳实践,以确保如果 Actions 使用了 Docker 缓存,则可以重新使用您的映像而不是重建。请参阅:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#leverage-build-cache 在构建镜像时,Docker 按照 Dockerfile 中指定的顺序逐个执行指令。在检查每个指令时,Docker 查找可重用的现有映像,而不是创建新的(重复)映像。这也意味着 GitHub Actions 的底层系统可以/将利用 Docker 缓存。
然而,编译和 Docker 等工具无法使用缓存机制,因此我建议您仔细考虑是否真的需要这个功能。另一种选择是从构件存储库(Nexus、NPM、MavenCentral)下载已编译/处理的文件,以跳过此步骤。您必须权衡收益与构建复杂性之间的关系。

1
谢谢您的帮助。在GitHub具备这个功能之前,我会回到使用Circle和Travis。:) - lpil
1
你能指给我Travis这样做的文档吗? - bitoiu
这是一个Dockerfile的示例,它利用了docker缓存来处理依赖项。值得一提的是,只有在每次构建镜像时运行在同一台机器上(因此使用相同的docker缓存)时,才会利用缓存。我不确定所有操作是否共享相同的“docker上下文”,但我认为这将是一个重要因素。 - Daniel
2
我刚刚尝试了一下,运行了我创建的一个基于Docker的操作5次。每次,Docker都是从头开始重新生成的。因此,看起来它并没有使用相同的Docker上下文(至少目前是这样)。我没有办法检查是否使用了相同的runner,所以也许那就是问题所在。总的来说-目前看来不是非常可靠的方法。 - AbdealiJK
如果您正在使用基于Docker的操作,则可能会发现我的答案很有用:https://dev59.com/8lMI5IYBdhLWcg3w4Pk_#58752958 - whoan

3
如果您在工作流中使用Docker,如@peterevans回答,GitHub现在支持通过cache操作进行缓存,但它有其局限性。出于这个原因,您可能会发现this action很有用,以绕过GitHub的操作限制。

免责声明: 我在GitHub正式支持缓存之前创建了该操作,并且由于其简单和灵活性,我仍在使用它。


1
有没有其他解决方案可以在不进行身份验证的情况下缓存层? - n1nsa1d00

1
我将总结这两个选项:
  1. 缓存
  2. Docker

缓存

您可以在工作流程中添加一个命令来缓存目录。当到达该步骤时,它会检查您指定的目录是否已保存。如果是,则会获取它。如果没有,就不会获取。然后,在进一步的步骤中,您编写检查以查看缓存数据是否存在。例如,假设您正在编译某个相对较大且不经常更改的依赖项。您可以在工作流程的开头添加一个缓存步骤,然后添加一个构建目录内容的步骤(如果它们不存在)。第一次运行时,它不会找到文件,但随后它会找到并且您的工作流程将运行得更快。

在幕后,GitHub正在将您的目录的 zip 上传到 GitHub 自己的 AWS 存储。他们清除任何早于一周或达到 2GB 限制的内容。

使用此技术的一些缺点是它只保存目录。因此,如果您安装到 /usr/bin 中,则必须缓存该目录!那会很尴尬。您应该安装到 $home/.local 中,并使用 echo set-env 将其添加到路径中。

Docker

Docker有点复杂,需要拥有dockerhub账户并管理两个东西。但是它更加强大。不仅可以保存目录,还可以保存整个计算机!您需要创建一个Dockerfile,其中包含所有依赖项,如apt-get和python pip行甚至长时间编译。然后构建该docker镜像并在dockerhub上发布。最后,您将设置测试以在新的docker镜像上运行,而不是例如ubuntu-latest。从现在开始,它将只下载镜像,而不是安装依赖项。
您可以通过将Dockerfile存储在与项目相同的GitHub存储库中,并编写包含步骤的作业来进一步自动化此过程。如果需要,它将下载最新的docker镜像,重新构建更改的步骤,然后上传到dockerhub。然后再编写一个需要该作业并使用该镜像的作业。这样,您的工作流程既可以更新docker镜像(如果需要),也可以使用它。
缺点是您的依赖项将在一个文件(Dockerfile)中,测试在工作流中,因此不是全部放在一起的。此外,如果下载镜像的时间超过构建依赖项的时间,则这是一个糟糕的选择。

我认为每个工具都有其优点和缺点。缓存只适用于非常简单的内容,例如编译成 .local 文件。如果需要更复杂的内容,Docker 是最强大的工具。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接