ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

GitHub Actions入门:从YAML工作流到自动化CI/CD部署实践

GitHub Actions入门:从YAML工作流到自动化CI/CD部署实践 1. 先说清楚GitHub Actions 到底是什么如果你写过代码、部署过项目大概率经历过这么几个场景代码改完手动打包上传服务器、测试用例本地跑完没问题一上生产就崩、每天凌晨得爬起来手动跑数据脚本。这些事情耗时、重复、还容易出错更要命的是每次都得靠人肉记忆去执行那套固定的流程。GitHub Actions 就是用来把这些机械化操作全部自动化掉的东西——你把流程写进一个 YAML 文件推到 GitHub 仓库剩下的事情交给它。它的本质是一个运行在云端的事件驱动执行环境。所谓事件驱动就是说仓库里的动作——比如 push 代码、提 Pull Request、创建 Issue、打标签——都会触发你预先定义好的一组任务。这些任务跑在 GitHub 提供的虚拟机上你不需要自己准备服务器也不需要配置复杂的 Jenkins 集群只要仓库是 GitHub 上的就能直接用。对比传统的 CI/CD 工具GitHub Actions 最大的优势是零基础设施成本注册账号就有免费的构建分钟数个人项目完全够用。这篇文章适合谁看如果你是一个独立开发者想给自己的开源项目加自动构建和测试或者你在小团队里维护项目受够了手动发版的流程又或者你听过 CI/CD 这个概念但一直觉得它很重、很复杂那么这篇内容就是写给你的。我不打算把官方文档从头到尾翻译一遍而是挑出真正用得上的核心知识配合完整的示例带你从零跑通第一条自动化流水线。2. 三个核心概念搞懂它们就理解了 Actions 的架构2.1 Workflow那条完整的自动化流水线Workflow工作流是 GitHub Actions 的最高层级概念它对应着仓库里.github/workflows/目录下的一个 YAML 文件。每个文件描述一条完整的自动化流程什么时候触发、在什么机器上跑、跑哪些任务。一个仓库可以有很多个 workflow 文件各自负责不同的场景——比如一个专门做测试一个专门做发布互不干扰。Workflow 这个名字起得很形象你可以把它理解成一张流水线图纸。图纸上画了工序、规定了每道工序在哪个工位完成、什么情况下开工。Actions 的执行引擎就是按图施工的工人。值得留意的是Workflow 的触发条件非常灵活可以用on字段控制比如只在特定分支推送时触发、只在 pull request 被合并时触发甚至可以定时触发。2.2 Job 和 Step流水线上的工位与动作一个 Workflow 内部由若干个 Job作业组成。Job 之间默认是并行执行的互不等待但如果你希望它们按顺序来比如先跑完测试再构建镜像就需要用needs关键字声明依赖关系。这个设计很有用可以大幅度缩短整体流水线的执行时间——不相关的任务没必要排队等着。每个 Job 里再细分是 Step步骤Step 是真正执行动作的最小单元。它可以是运行一行 shell 命令也可以调用一个别人封装好的动作Action比如actions/checkout负责拉取你的代码。Step 按照从上到下的顺序依次执行任何一步失败默认整个 Job 就会中止。这个行为逻辑简单直接符合大多数构建场景的预期——测试没跑过就没必要继续打包了。2.3 Event 和 Runner触发开关与执行环境Event 是触发 Workflow 的事件常用的包括push、pull_request、schedule定时、workflow_dispatch手动触发。值得单独说的是workflow_dispatch它允许你在 GitHub 网页上点击按钮来手动运行工作流这在调试和应急执行时特别方便——不是所有事情都应该自动化的有时候你需要一个手动挡。Runner 是实际执行任务的虚拟机环境。GitHub 默认提供 Ubuntu、Windows、macOS 三种系统的 runner。Ubuntu 是最常用的因为免费且大部分开源工具链都支持。每个 runner 都是全新的环境上一次运行留下的文件不会残留——这是好也是坏好的是环境隔离很干净坏的是每次都要重新装依赖所以学会缓存依赖会是你后面优化执行时间的重点。核心概念一句话解释类比Workflow一条完整的自动化流程一张生产流水线图纸Job流程里的一个任务单元流水线上的一个工位Step任务里的最小执行动作工位上的一次具体操作Event触发工作流的事件按下流水线的启动开关Runner执行任务的虚拟机工位上的工人和设备3. 动手写第一个 Workflow从零到跑通的完整过程3.1 准备工作与目录结构你不需要新建一个专门的项目来学习这个直接在任意一个已有的 GitHub 仓库里操作就行。如果还没有仓库先在 GitHub 上创建一个空的代码仓库随便放一个文件比如README.md。然后你需要在仓库根目录下新建这个路径.github/workflows/。注意前面有个点这是一个隐藏目录GitHub 只会识别这个固定路径下的 YAML 文件作为工作流定义。目录里每个.yml或.yaml文件都代表一条独立的工作流文件名可以随便起但建议用含义清晰的名字比如ci.yml、deploy.yml。创建好目录后我们来写第一个文件.github/workflows/ci.yml。你可以直接在 GitHub 网页上创建文件也可以克隆到本地用编辑器写完后推送两种方式效果一样。3.2 第一个完整示例push 时自动运行测试直接看完整内容我逐行解释name: CI on: push: branches: [ main ] pull_request: branches: [ main ] jobs: test: runs-on: ubuntu-latest steps: - name: 拉取代码 uses: actions/checkoutv4 - name: 设置 Node 环境 uses: actions/setup-nodev4 with: node-version: 20 - name: 安装依赖 run: npm install - name: 运行测试 run: npm test这个文件做的事情是只要有人向main分支推送代码或者提交了针对main分支的 pull request就在一台全新的 Ubuntu 虚拟机上依次执行拉代码、装环境、装依赖、跑测试这四步。name字段是给工作流起个显示名字会出现在 GitHub 的 Actions 标签页里方便你识别。on字段定义了触发条件格式为事件: [目标]这里写了两种事件用 YAML 的列表语法并行生效。jobs下面是作业定义test是我们自己给这个作业起的名字可以随意替换。runs-on指定运行环境。steps列表中的每个元素就是一个步骤uses表示复用现成的 Action 组件run则是直接执行命令。3.3 提交后能看到什么把文件推送到 GitHub然后打开仓库页面上的 Actions 标签页。你会看到一个名为 CI 的工作流开始运行点进去可以看到它正在执行。每个步骤运行过程中会实时输出日志如果某一步失败日志会标红并能看到具体的错误输出。第一次跑的时候我建议你故意断在一个步骤上看效果——比如把npm install改成一个不存在的命令观察失败的样子这比一切顺利更能让你理解排查流程。UI 上每个 Job 的执行过程很直观有点像一个待办事项清单完成一项打一个勾失败一项立刻停止。你可以自由地展开每个 Step 看命令行输出这在排查问题时是最重要的入口。4. 语法细节与进阶用法让你的工作流真正好用4.1 环境变量与 Secret把敏感信息藏起来工作流里经常会用到一些非公开的信息比如服务器的 SSH 私钥、云服务的 AccessKey、API Token。这些内容绝不能直接硬编码写在 YAML 文件里因为仓库一旦公开就等于把密钥拱手送人。GitHub 提供了 Secrets 机制位置在仓库的Settings - Secrets and variables - Actions页面。添加 Secret 后在 YAML 中使用${{ secrets.XXX }}语法引用。比如一个典型的部署场景- name: 部署到服务器 env: HOST: ${{ secrets.DEPLOY_HOST }} USERNAME: ${{ secrets.DEPLOY_USER }} run: | ssh $USERNAME$HOST cd /var/www git pull运行时这些变量会注入到执行环境中日志输出时会自动对你的 Secret 做掩码处理避免泄露。但还是要有安全意识不要在run命令里显式地echoSecret 值即使 GitHub 做了掩码也是一种坏习惯。环境变量的另一个用法是给不同环境开发、测试、生产提供同一工作流的不同配置通过env字段在 Job 或 Step 级别覆盖。这个层级覆盖逻辑是Step 级别的env优先级最高然后是 Job 级别最后是 Workflow 级别。4.2 缓存依赖把执行时间从五分钟降到半分钟反复安装依赖很浪费时间。GitHub 官方为主要生态提供了缓存 Action比如actions/cache。原理是给依赖目录生成一个 key如果 key 没有变化就把之前缓存的内容直接恢复跳过重新下载的耗时过程。Node.js 项目的依赖缓存配置- name: 缓存 npm 依赖 uses: actions/cachev4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles(**/package-lock.json) }}path指定要缓存的目录key是缓存的唯一标识。hashFiles(**/package-lock.json)会根据锁文件内容生成哈希值这意味着只要依赖没有变化缓存就能命中一旦 package-lock.json 有改动key 就变了会缓存一份新的。这里要注意缓存的是依赖的下载缓存而不是node_modules本身。因为node_modules目录很大、不稳定在不同系统间复用容易出环境差异问题而缓存 npm 的全局缓存目录让npm install走本地缓存已经能获得绝大部分的加速效果了。实测下来热缓存能够把含 500 个依赖包的安装时间压缩到原来的 15% 左右。4.3 矩阵构建一次跑多个版本如果你的项目需要兼容多个 Node 版本、多个操作系统可以在 Job 级别声明strategy.matrix。这个语法会用声明中的每种组合各生成一个独立的执行实例jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest] node-version: [18, 20] steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test上面的配置会生成 4 个组合2 个系统 × 2 个 Node 版本每个组合独立运行矩阵中的变量通过${{ matrix.xxx }}格式引用。这个功能在维护开源库或者对兼容性要求较高的项目上非常有用你会发现多环境验证的成本远低于自己搭多台机器。4.4 手动触发与定时触发定时触发的语法使用 cron 表达式on: schedule: - cron: 0 2 * * *这表示每天凌晨 2 点执行一次。注意GitHub Actions 的定时任务有延迟超过预定时间 30 分钟以内都算正常。如果需要精准到分钟级的定时需要结合外部调度服务来触发。手动触发使用workflow_dispatchon: workflow_dispatch:加了后仓库 Actions 页面会出现一个 Run workflow 按钮点开下拉框可以指定分支运行。要注意workflow_dispatch只能触发默认分支或指定分支上的工作流文件在非默认分支想测试工作流需要先合并进去或改触发条件。5. 一个完整的实战案例自动构建并发布到 Pages5.1 场景与需求分析假设你维护一个基于 Vite 的静态站点希望每次 push 到main分支后自动运行构建命令把打包产物发布到 GitHub Pages 上实现推送即发布。同时我们希望只有当测试通过时才发布如果测试失败流程自动中断不产生任何副作用。这个场景非常典型——它把测试、构建、部署三个环节串起来了也是很多小团队真实在用的发布方式。5.2 完整的工作流定义name: Build and Deploy on: push: branches: [ main ] permissions: contents: read pages: write id-token: write concurrency: group: pages cancel-in-progress: true jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm install - run: npm run build - uses: actions/upload-pages-artifactv3 with: path: ./dist deploy: environment: github-pages needs: build runs-on: ubuntu-latest steps: - uses: actions/deploy-pagesv4前几段你可能都认识了重点说几个新增的关键点。permissions设置工作流的权限级别contents: read表示只读代码仓库内容pages: write允许上传到 Pagesid-token: write是 GitHub Pages 部署所要求的身份验证权限。这个声明不是可选的省略或配置错误会导致部署步骤报 403 权限错误。concurrency是一个很有用的部署保护它定义了名为pages的并发组如果有新的推送触发了新运行之前还没跑完的旧运行可以直接取消。这样避免了部署任务互相打架也省了构建分钟数。这个例子里deploy作业声明了needs: build含义是 deploy 必须在 build 成功后才执行。如果你把deploy和build并列而不加needs它们会同时跑而 deploy 拿不到 build 的产物——这是一个新手最容易踩的坑。另外注意actions/upload-pages-artifact的作用是在作业之间传递构建产物这里是通过 artifact 在云上暂存不是写在某个共享磁盘里。5.3 实操后验证效果推送这个工作流文件后在 Actions 页面观察它执行两遍。第一遍模拟部署成功第二遍故意在npm run build前加一行run: exit 1人为制造失败观察整个工作流是否如预期停在了 build 阶段、deploy 是否被跳过。这个实验会帮你建立对工作流拓扑的直觉一个 Job 失败后依赖它的 Job 会进入 skipped 状态而不是同时执行。GitHub Pages 的部署完成通知在 Actions 页面的 deploy 日志里会输出一个 Pages deployment 的网址打开就能看到你的站点。以后每次推送更新几分钟后站点就自动更新了。6. 常见问题与排查技巧我把踩过的坑都写在底下6.1 权限问题403 和 Resource not accessible这是被问得最多的一个。出现这个报错时先去检查你的工作流文件里有没有声明permissions。很多仓库默认的 GITHUB_TOKEN 权限很受限尤其是涉及发布 Release、写 Pages、操作 Package 时一定要显式声明需要的权限。另外如果你用的是 GitHub 免费账号从私有仓库触发的工作流无法使用某些需要写权限的操作这是账号层面的限制。解决办法是检查仓库的 Settings - Actions - General 里的 Workflow permissions 设置改成 Read and write permissions。6.2 工作流没被触发最常见的原因文件推了但 Actions 页面根本看不到新的运行记录这是很多人第一部就卡住的地方。我帮别人排查这种问题90% 的情况出在两个地方文件名写错了或者目录放错了。workflows目录必须放在.github下名字不能多了一个 s.github/workflow/是无效的。另一种情况是分支限制。比如你写了branches: [ main ]但推送的是 develop 分支自然就不触发。这时候可以临时用workflow_dispatch手动触发来验证次要分支上的改动不要直接把触发条件改成branches: [**]——这会带来不必要的运行开销。6.3 步骤失败但不知道从哪查起Actions 页面的日志大且乱尤其是依赖安装一步的输出可能有几百行。我的建议是先看 Summary 页面它会给出一个简明的、几行字的错误概述然后点开第一步失败的日志去搜error关键字。日志右上角还有个 copy 按钮可以快速拷贝完整日志去搜索引擎找答案——把报错原文拉出来搜往往一分钟内就能找到解决方案别自己瞎猜。6.4 执行时间太长优化策略运行时间直接关系到配额消耗和等待成本。优先检查是不是没有开缓存其次看是不是每个 Job 里都重复npm install了。合理的策略是将构建和测试放在同一个 Job这样依赖只需安装一次如果确实需要多 Job则把依赖打包成 artifact 传递但这会增加序列化和下载时间通常只在 Job 必须在不同系统上跑时才值得。6.5 常见错误速查表错误现象可能原因解决思路找不到.github/workflows目录名拼错、大小写错严格核对路径GitHub 区分大小写Secret 读取为空变量名拼错、Secret 建在别的层级检查 repository 和 environment 两级的 Secret缓存一直不命中key 设置不当确认 key 包含能反映依赖变化的 hashFiles 结果构建失败但本地通过环境系统不同检查工作流里是否锁定了 Node 版本多个 Job 拿不到彼此的产物没有用 artifact 传递使用upload-artifact和download-artifact定时任务不准时GitHub 调度延迟延迟 30 分钟内正常可改用 workflow_dispatch7. 一些只在实际操作后才会懂的经验分享GitHub Actions 最容易被低估的点是它的免费额度其实很够用。GitHub 免费账户每月提供 2000 分钟的 Linux runner 时间个人项目如果只是跑测试和构建基本用不完。真正要留意的是 Windows 和 macOS runner它们的倍数消耗比较快如果有矩阵构建需求建议把重点组合放在 Linux 上只在必要的时候补测其他系统。还有个小技巧我一直在用把所有工作流文件的第一个 Step 都设置成加一个timeout-minutes上限。默认情况下单个 Job 最多跑 6 小时但对大部分项目来说 15-30 分钟已经绰绰有余。显式声明这个值的好处是万一某个步骤因为依赖卡死或者网络问题挂起了工作流会及时报错退出而不是消耗大量配额等超时。比如这样jobs: build: timeout-minutes: 30 runs-on: ubuntu-latest最后再分享一个组织工作流文件时的心得一个仓库里如果有多个工作流尽量让每个文件只负责一个关注点——测试、部署、定时任务各管各的。这看起来会让文件数量变多但排查问题时你会发现非常舒服。每次进 Actions 页面一眼就能看出哪个流程错了不需要点开巨大的 YAML 顺着跑。Cron 任务单独放更是个好习惯因为它的触发时间和代码推送完全不同混在一起会让日志很难翻。我开始也只是把 Actions 当成一个自动化测试工具来用跑通了以后才慢慢尝试部署、定时提醒、自动打标签这些场景。等你在一条工作流上成功了后面复制这个模式去解决别的问题会非常顺手。
返回列表