
如果你维护过哪怕一个有点用户量的项目大概率经历过这种场面发版前先在本地把测试跑一遍再手工打一次包传到服务器然后 SSH 进去重新部署。过程不复杂但每次都一样而且无数次验证了同一件事——人做重复的事一定会出错忘了跑测试、漏传文件、部署到一半网络断了。GitHub Actions 就是替你把这一切写成代码、随仓库一起保存、到点自动执行的工具。它不需要单独部署一台 Jenkins不依赖你本地环境只用一份 YAML 文件定义流程push 代码的瞬间任务就在 GitHub 的服务器上跑起来。这篇文章不打算讲花哨的语法糖。我会从自己的使用经验出发把它拆成几块它到底解决什么问题、第一份 workflow 怎么写、怎么从测试一路走到自动部署、以及运行的时候会踩到哪些坑。适合谁看写过 Git、对 CI/CD 有概念但没上手的人或者用过其他 CI 工具、想看看 GitHub 这套有什么不同的人。看完之后你应该能独立写出带测试、打包、部署的完整流程。1. 先搞清楚 GitHub Actions 到底在帮你做什么1.1 它消灭的是什么样的重复劳动我最早对自动化的需求特别朴素每次 push 代码之后想自动跑一遍测试每次打个v1.0.0的 tag想自动生成一份 release notes每次凌晨两点想把线上数据抓下来存到仓库里做分析。这些事单独看都不难难在“每次都记得做”。GitHub Actions 解决的就是这个“记得”的问题。你定义好触发条件剩下的交给它往 main 分支推代码就跑测试提 PR 就做静态检查定时任务到点就执行打 tag 就自动发版。它像厨房里的智能电饭煲设定好流程到点自己做不会忘。对比下来手动流程的痛点非常清晰依赖某个人的本地环境别人跑不起来看不到历史记录出了问题说不清哪一步错的人容易偷懒时间一长步骤就变样了。放进 GitHub Actions 之后流程变成了仓库的一部分。任何人 checkout 代码都能看到.github/workflows/里躺着什么任务每一步的日志都能回放谁改了流程也清清楚楚。这本身就是一种工程规范把“口头约定”升级成“代码约定”。1.2 一次性搞懂核心概念Workflow / Job / Step / Runner / ActionGitHub Actions 的核心概念不多但第一次接触容易被术语绕晕。我用自己的话说一遍。Workflow一份.yml配置文件放在.github/workflows/目录下。它定义“什么时候开始干活”和“具体干什么活”。Event触发 Workflow 的事件。最常见的push、pull_request还有手动触发workflow_dispatch、定时触发schedule。JobWorkflow 里的一个执行单元。一个 Workflow 可以有多个 Job默认情况下它们在各自的 runner 上并行跑。StepJob 里的具体动作。一个 Step 可以是一条 shell 命令也可以是一个 Action。Runner真正干活的机器。通常用 GitHub 托管的ubuntu-latest也可以用 Windows、macOS甚至你自己维护的自托管机器。Action别人封装好的“积木”。比如actions/checkoutv4负责把代码拉下来actions/setup-nodev4负责装 Node.js。你不用自己写全套命令直接uses就行。串起来就是某个 Event 发生了GitHub 在 Runner 上启动一个或多个 Job每个 Job 按顺序执行一串 StepStep 里可以夹带命令和 Action。就这么简单。概念虽然简单但我建议你在写第一个 Workflow 前先在仓库的Actions 标签页里点开几个别人项目的运行记录看一遍。你会直观看到“哪一步花了多少秒”“哪一步挂了”“日志长什么样”比读十篇文档都有用。1.3 为什么个人项目也值得上 Actions很多人觉得“自动化工具是大团队才需要的”其实个人项目恰恰是收益最大的场景。免费额度对个人完全够用公共仓库免费跑私有仓库每个月也有至少 2000 分钟的免费额度。对个人开发来说一个月能跑掉这 2000 分钟说明你的项目已经活跃到相当程度了。另一个理由是生态。GitHub Marketplace 上有几十万个 Action从部署到服务器、发邮件通知、生成二维码、同步到云存储都有现成的。你写 CI 时基本不用从零发明搜一下、配一下、点个赞就能用起来。还有一点很实际它和代码评审绑在一起。PR 页面上直接显示 CI 是否通过维护者看到绿色勾勾才敢点 Merge。对一个开源项目来说这比在 README 上贴一堆“质量达标”的标签更有说服力。对团队来说它也让“测试在本地通过了”这句话不再具有意义——所有检查都跑到云端统一环境里做本地过了不算数。2. 从零写第一个 Workflow请抓牢这三个字段2.1 新文件放哪里以及 YAML 书写避坑工作流文件必须放在仓库根目录下的.github/workflows/里文件名随意.yml和.yaml后缀都行。我的习惯是按用途命名纯测试的叫ci.yml部署相关的叫deploy.yml定时任务叫schedule.yml。一个仓库可以放多个文件GitHub 会全部识别。第一行建议先写一个名字name: CI之后的核心字段只有一个别的都能查文档补。编码时我建议专心把握三个字段on什么时候跑、jobs跑什么、steps怎么跑。YAML 是这类文件的硬门槛第一次写很容易栽在格式上。几个血泪经验缩进必须用两个空格不要用 Tab。连续多层嵌套时一乱YAML 解析器直接报错。冒号后面必须跟一个空格比如name: CI而不是name:CI。列表项用-开头和下面的 key 保持同一级缩进。字符串如果包含特殊字符比如*、{最好用单引号包起来。我见过太多“日志提示 YAML syntax error”的情况十有八九是缩进问题。提交前养成一个好习惯在本地编辑器里开启 YAML 语法高亮或者直接用 VS Code 的插件检查格式。2.2 触发事件on 字段的几种常用姿势on字段决定了工作流什么时候启动它是 Workflow 的“开关”。最常见的写法是on: push: branches: - main pull_request: branches: - main workflow_dispatch:push在推代码时触发配合branches过滤。只写branches: [main]就是说只有 push 到 main 分支才跑。pull_request在提 PR 和更新 PR 时触发。默认行为是 opened、synchronize、reopened 三种事件都会触发一般够用。workflow_dispatch是手动触发。加上它之后Actions 页面右上角会出现一个 “Run workflow” 按钮随时能手动跑一次。这个入口强烈建议每次都留着后面调试会感恩自己当时没偷懒。还有两个容易被忽略的过滤参数on: push: paths: - src/** - .github/workflows/*paths表示只有改动指定路径时才跑适合“只改 README 就不跑 CI”的场景能省不少分钟数。我个人的项目里文档、图片、配置类的改动根本不需要触发完整测试线。定时任务用scheduleon: schedule: - cron: 0 2 * * *这里有个几乎人人中过的坑cron 用的是 UTC 时间。如果是北京时间得在 UTC 基础上加 8 小时。想每天北京时间上午 10 点跑cron 就要写0 2 * * *。我早年没注意时区定时任务连着好几天都在“错误的凌晨”执行。2.3 一个足够日常用的 Node.js CI 示例下面这份配置是我个人项目的模板注释标了每一段的用途name: CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: build: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: 拉取代码 uses: actions/checkoutv4 - name: 安装 Node.js uses: actions/setup-nodev4 with: node-version: 20 cache: npm - name: 安装依赖 run: npm ci - name: 跑测试 run: npm test - name: 构建 run: npm run build逐行拆开说runs-on: ubuntu-latest是 runner 选择最常用、免费额度内最稳。timeout-minutes: 20是个好习惯防止某个 Step 卡死白白吃掉一两个小时。第一步actions/checkoutv4几乎是所有工作流的第一步它把当前仓库代码拉到 runner 上。没有它后面的命令面对的是空目录。第二步actions/setup-nodev4负责装 Nodewith.node-version指定版本with.cache: npm会自动缓存~/.npm下次安装依赖会快很多。npm ci是我强烈推荐的安装方式。它和npm install的区别在于它严格按package-lock.json安装删除node_modules后全新安装保证生产构建和本地高度一致。前提是仓库里有 lock 文件。如果没有第一次需要先运行npm install生成一份并提交。跑测试和构建没什么特殊之处就是普通命令。失败时这个 Step 会标红整个 Job 自动失败PR 上直接显示红色叉号。如果需要在多个 Node 版本下验证intro 一下 matrixstrategy: matrix: node-version: [18, 20, 22]它会让这个 Job 在三个版本下各跑一遍适合给兼容性要求高的库用。实际要写很多版本时我一般收窄到最常用的 2-3 个避免节奏被拖慢。3. 从 CI 走向 CD把自动部署也写进去3.1 部署方案选定一种别一开始就追求复杂CI 跑通之后下一步自然是“测试过了就直接上线”。GitHub Actions 做部署的思路和本地手动部署没什么两样只是把命令搬到了云端。最常见也最好上手的模式Workflow 通过 SSH 连到服务器在服务器上执行拉代码、装依赖、重启服务。另一种模式是服务器主动拉取服务器上挂一个 webhook 或定时脚本检测到仓库有更新就自己拉代码。这种方式对 GitHub Actions 的依赖更少但多了部署机的配置成本。对个人项目来说没有分布式、没有多节点我建议先选 SSH 模式直观、容易排查。为什么不直接把整个代码目录塞到服务器上因为生产环境通常需要经过构建、压缩、迁移这些动作在服务器上执行更可控。GitHub Actions 负责触发和传递参数最终落地由服务器完成。3.2 配置 Secrets这一步决定安全性关键问题是SSH 私钥、服务器密码这些敏感信息绝对不能写进.yml文件也不能出现在日志里。GitHub 提供了 Secrets 功能。仓库页面进入Settings → Secrets and variables → Actions点击 New repository secret添加一个键值对。之后在 workflow 里用${{ secrets.XXX }}引用。我一般会建这几个Secret 名称用途SERVER_HOST服务器 IP 或域名SERVER_USERSSH 登录用户名SSH_PRIVATE_KEY部署用的 SSH 私钥处理私钥时有一个特别容易翻车的细节粘贴私钥如果带有换行GitHub Secrets 表单会自动按单行处理一部分导致 PEM 格式被破坏登录时直接报 “Permission denied”。我的做法是先在本地把私钥文件 base64 编码或者用cat ~/.ssh/deploy_key原样复制粘贴到 Secrets 时确保不手动改任何字符多测几次就稳了。另外Secrets 有个安全限制来自 fork 仓库的 PR 默认无法读取仓库的 secrets。外部贡献者提的 PR 不能通过工作流直接拿到你服务器的 access token。这是 GitHub 的安全设计不是 bug。遇到这种情况通常的处理方式是对 fork PR 只跑测试类 CI不跑部署类任务或者等代码合并进 main 分支后再由 main 上的 workflow 执行部署。3.3 一个可以直接抄的部署示例下面这份deploy.yml假设代码在服务器/var/www/myapp目录使用 PM2 管理进程仓库私钥存在SSH_PRIVATE_KEY里。name: Deploy on: push: branches: - main paths: - app/** - .github/workflows/deploy.yml concurrency: group: production cancel-in-progress: false jobs: deploy: runs-on: ubuntu-latest environment: production steps: - name: 拉取代码 uses: actions/checkoutv4 - name: 部署到服务器 uses: appleboy/ssh-actionv1.0.3 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd /var/www/myapp git pull origin main npm ci npm run build pm2 restart myapp这里有几个值得留意的细节concurrency是一个非常容易被忽略的字段。它的作用是防止两个 Job 同时跑同一套流程。比如你连续 push 两次第二次还没等第一次部署完就开始两个 Job 同时在服务器上 git pull很容易出现代码错乱。我设置cancel-in-progress: false是为了让部署任务排队完成而不是被打断。CI 类任务则相反通常设true因为旧测试没必要跑完。environment: production声明这是一个环境级别的部署。配合 GitHub 的环境保护规则你甚至可以设置成 “main 分支发生变更后还需要人工批准才真正执行”。对个人项目来说有点重但对团队项目这是防止误操作的关键防线。appleboy/ssh-actionv1.0.3是社区里广泛使用的 SSH 执行工具。你也可以不用它直接跑ssh userhost cd /var/www/myapp git pull origin main npm ci npm run build pm2 restart myapp效果一样。区别在于它自动处理了 key 认证、连接保持、超时控制这些麻烦事。在服务器端一定要保证 git 拉取不使用密码。推荐在服务器上为部署用户单独配一个deploy key只读访问仓库这样git pull不会因为密码输入而卡住。3.4 把它变成积木Reusable Workflows 的初体验当你有多个项目要维护会发现部署逻辑高度相似拉代码、装依赖、跑构建、发通知只是仓库名和服务器地址不同。复制粘贴当然能解决问题但每次都要改一堆行而且模板升级时要在每个仓库里同步改一遍。GitHub 的 Reusable Workflows 能解决这个问题。它可以被其他 Workflow 调用接收参数和 Secrets把公共逻辑收拢到一处。被复用的 workflow 文件开头要这样写name: Reusable Deploy on: workflow_call: inputs: deploy_path: required: true type: string secrets: SERVER_HOST: required: true SERVER_USER: required: true SSH_PRIVATE_KEY: required: true jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: appleboy/ssh-actionv1.0.3 with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USER }} key: ${{ secrets.SSH_PRIVATE_KEY }} script: | cd ${{ inputs.deploy_path }} git pull origin main npm ci npm run build pm2 restart myapp然后在普通 workflow 里调用它jobs: call-deploy: uses: yourname/yourrepo/.github/workflows/reusable-deploy.ymlmain with: deploy_path: /var/www/myapp secrets: SERVER_HOST: ${{ secrets.SERVER_HOST }} SERVER_USER: ${{ secrets.SERVER_USER }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}变化点通过with传入敏感信息通过secrets传入。团队的公共部署逻辑只要维护一个仓库其他项目引用即可。我实际用下来的感受是前几次写麻烦一点等维护到第三个项目时省的心是成倍增长的。4. 高频报错与排查记录这些坑我替你踩了4.1 排查报错的基本思路遇到 Actions 报错时第一个动作永远是打开运行记录页面看具体是哪一步失败了。日志是逐条输出的红色日志上方通常有一条粗略的错误摘要。我总结了三个步骤看日志定位是 YAML 解析失败、命令报错还是权限问题。前 10 行日志里基本能判断方向。本地复现把 failed Step 里的命令拉到本地跑一次。很多 CI 报错只是因为环境变量、Node 版本或 lock 文件差异本地跑通后再反过来对照 workflow 里的上下文。加打印在不影响安全的前提下把关键变量env或文件路径打印出来。每次排错时我都会临时加一行run: env或run: ls -la看看环境状态。另外一个小习惯在 Step 里指定 shell 时把调试模式打开。比如run前面加shell: bash -x {0}脚本里的每一行命令都会回显出来一目了然。注意日志可能暴露密钥调试完务必删除。4.2 高频问题速查表现象常见原因解决方法Workflow 完全没有运行on里分支名写错或格式不对检查默认分支名main 还是 master加上workflow_dispatch手动重跑验证日志报mapping values are not allowedYAML 冒号后缺少空格全文检查冒号和空格尤其注意多层嵌套npm ci报 lock 文件不存在仓库没有提交package-lock.json本地先npm install生成 lock 文件并提交${{ secrets.XXX }}打印出来是空fork 的 PR 拿不到 secrets环境变量命名拼错确认运行环境是否安全检查 Secret 是否在正确仓库SSH 部署报Permission denied (publickey)私钥和公钥不匹配服务器用户不对私钥格式坏了重新上传正确的私钥确认服务器用户与公钥一致定时任务不执行cron 时区记错仓库不活跃写在非默认分支换算北京时间确认 schedule 在默认分支手动触发一次排除工作流本身问题Job 长时间排队私有仓库并发配额受限减少冗余 workflow用concurrency合并重复运行磁盘空间不足缓存和日志累积过多在 Job 末尾加清理步骤或缩短缓存保留时间这张表覆盖了我被问到最多的几类问题。第七行“Job 长时间排队”在小团队里不太明显但如果你把 Actions 用到极致比如每个 push 都触发好几个 workflow私有仓库免费额度下的并发限制很快会让你体验一把“排队两小时构建五分钟”的感觉。4.3 本地模拟用 act 减少反复提交Actions 有一点很烦人每次调试都要 push 一次代码才能在 GitHub 服务器上跑。如果 workflow 逻辑复杂改一次、提交一次、等两分钟一天下来几十次 commit 就为了看一个结果效率低且日志混乱。有个社区工具叫actnektos/act它基于 Docker 在本地模拟 GitHub Actions 的运行环境。用它可以执行本地目录里的工作流无需真正推到 GitHub。安装很简单brew install act然后进入项目目录act -j build-j build表示只跑名为 build 的 Job日常调试时省时间。它默认会下载 Docker 镜像首次运行较慢第二次之后就有缓存了。用 act 时要注意它不会完全复刻 GitHub 的某些行为比如 repository secrets、环境变量、某些官方 action 的细节行为。但对run: npm ci、run: npm test这类核心逻辑的验证完全够用。我自己的习惯是先在本地跑 act 确认逻辑无问题再 push 到 GitHub 上看最终结果。这个“先本地后远程”的操作习惯让我省掉了大量无效 commit。act 无法完整模拟 secrets所以涉及部署类的 job 我一般不会用 act 跑只在本地验证纯构建、纯测试类 job。5. 从会用到好用几个建议你早点养成的习惯5.1 控制资源成本的两个开关免费额度再多也是有限的钱。我见过不少人一个月把 2000 分钟跑穿因为每个 push 都触发全量 CI、每个 job 都要开 4 个 step 的缓存重建。两个开关能立竿见影第一个是concurrency。设置 work 路由组后同一时间只保留一个运行实例这样频繁 push 时旧任务会自动取消不会堆积。concurrency: group: ci-${{ github.ref }} cancel-in-progress: true第二个是paths过滤。只改文档、只改配置时不触发完整构建链。我在 2.2 节已经给了写法这里再强调一次它能让你省下至少 30% 的分钟数而且“该跑的时候才跑”更符合直觉。5.2 自动化也要可观察、可回退把部署全交给 Actions 之后风险也变了以前是我手动操作可能忘现在系统自动化可能错而且会安静地错到不可收拾。所以必须配上可观察机制。至少做三件事失败通知在 workflow 末尾加一个if: failure()的 Step用邮件或钉钉 Webhook 把失败信息送出去。这样线上有问题时你不用每次主动去刷 Actions 页面。部署前备份在服务器上的部署脚本里把发布前目录打个 tar 包或做数据库备份。自动化越顺手越要给自己留一条回退的路。保留重跑能力记得给每个 workflow 留workflow_dispatch它不只用于调试也用于“线上挂了手动热修复后立刻重新部署”的紧急场景。if: failure()的实现大致这样- name: 发失败通知 if: failure() run: curl -X POST https://your-webhook-url -d {msg: CI failed}5.3 给 workflow 做减法保持可维护我见过一团乱的 workflow一个 job 里塞了二十个 step从安装依赖到发通知全挤在一起另一个极端是滥用复用把五个项目塞进一个 workflow牵一发而动全身。理想状态是“既按职责拆开又不碎片化”。我的习惯拆法CI workflow代码进来就跑测试执行频率高内容稳定。CD workflow部署到指定环境。通过environment区分 staging 和 production通过needs依赖 CI 的结果CI 不通过就不部署。Schedule workflow专门放定时任务跟 push 触发完全隔离。这样拆开之后每个文件职责单一修改某一类逻辑时不用在大文件里找半天。Action 版本也要管好。uses: actions/checkoutv4里的v4是主版本号跟上大版本升级是基本操作想完全锁定可以用commit-sha做不可变引用。社区 action 和维护者的公信力密切相关尽量不要用下载量少到可疑的冷门 action特别是它要读取你有权限的 token 的时候。最后无论 workflow 多顺手都要理解“自动化不是银弹”。我自己的习惯是每个仓库至少保留一个手动触发入口和一个查看实时日志的习惯假如哪天自动环节老化、失败范围扩大至少还能靠人踩刹车。把自动化当成可靠的工具而不是不需要思考的替身这才是能干得长远的心态。