ARTICLE DETAIL

资讯详情

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

GitHub Actions CI 实战:工作流、缓存、权限与部署优化

GitHub Actions CI 实战:工作流、缓存、权限与部署优化 打开任何一个稍微规范点的 github 仓库根目录下大概率能翻到一个叫.github/workflows的文件夹里面躺着几个 YAML 文件名字通常叫ci.yml、build.yml、release.yml。这就是 GitHub 自带的持续集成能力在日常项目里的落脚点——GitHub Actions 承载的 CI 流程。它干的事情说出来特别朴素代码一推上去远端机器自动拉代码、装依赖、编译、跑测试、检查代码风格、打包产物有任何一环不过直接在提交记录旁边挂个红叉并把日志摊开给你看全过了就挂个绿勾你再去合并分支心里就有底。我第一次把 CI 接进项目是给一个三人小组的后端仓库做测试卡点当时最直接的动机很功利不想再出现我本地跑得好好的这种扯皮。后来用得多了才发现CI 真正的价值不只是跑测试它把很多口口相传的规矩变成了可执行、可追溯、可复现的流程。这篇内容适合三类人看刚学会 git 提交、想给个人项目加个自动化检查的新手团队里被要求搭流水线、但不知道从哪下手的开发者以及已经会用一点 GitHub Actions、但配置总是慢、总是失败、总想优化一下的老手。下面我按自己的实际搭建顺序把思路、配置、实操和踩过的坑一次说透。1. 先把思路理清CI 挂在仓库上到底图什么很多人上来就抄一份 YAML 改改路径能跑起来就算完事结果后面遇到缓存失效、额度暴涨、fork 的 PR 拿不到密钥这些情况就懵了。我更建议先花十分钟想明白为什么把它放在仓库里而不是放在别的地方。1.1 三个被反复踩中的痛点第一个痛点是验证的滞后性。没有 CI 的时候代码质量靠什么保证靠人。靠 reviewer 眼睛看靠测试同学手动点。这中间的时间差可能是几个小时甚至几天等到发现问题的那个时刻写代码的人已经切换到别的任务上去了重新捡起上下文的心智成本极高。CI 把验证提前到了提交的那一刻谁写的谁看日志反馈闭环从天缩短到分钟。第二个痛点是环境不一致。本地装着 Python 3.9同事装着 3.12测试机上装的是 3.10一段依赖版本的代码在三个人机器上表现不同排查起来能耗掉一整个下午。CI 的 runner 是一台干净的标准机器每次都是全新环境这就把环境变量这个隐藏因素基本消灭掉了。第三个痛点是流程不可见。项目里总有那么几条隐形规则合代码前必须跑通测试、发版前必须改版本号、依赖不能随便升级。写在文档里的人不会看写在 CI 里的机器一定会执行。这是我认为 CI 最有价值的地方——它把团队共识固化成代码。提示如果你的项目连一个自动化测试都没有也别急着放弃 CI。哪怕只做依赖能否安装成功代码能否编译通过配置文件是否是合法 YAML这三件事也能挡掉相当一部分低级事故。1.2 为什么最后落到 GitHub Actions 上市面上的方案不少自建 Jenkins、用别的代码托管平台自带的流水线、买商业 CI 服务都能解决问题。但多数人最后还是选了 GitHub Actions理由很实际。和仓库是原生的。不需要单独部署一套服务不需要维护构建节点的系统补丁仓库即配置源。权限体系、Secrets 管理、状态回传、PR 里的检查项显示全都是现成的不用自己对接 webhook 和 OAuth。生态足够厚。官方和社区维护的 action 覆盖了绝大多数常见场景签出代码、装各种语言运行时、缓存依赖、登录镜像仓库、上传产物、发通知。你想做的绝大部分事情都已经有人封装好了调用一行uses就行。免费额度对个人和小团队够用。公开仓库的构建基本不受限私有仓库按账号类型给一定额度学习和小规模使用没什么压力。唯一要注意的是不同操作系统的计费倍率不一样Linux 最便宜macOS 明显更贵别拿 macOS 的 runner 去做本该在 Linux 上跑的事。当然它也有不适用的场景极重的构建任务、需要特殊硬件、对构建节点有强内网依赖的项目往往还是自建 runner 或者另起一套更合适。我的经验是先用托管 runner 把流程跑顺等真的撞到性能或网络瓶颈再考虑挂自建机器。1.3 一条能打的流水线至少要有哪几段别追求一步到位。我给团队搭 CI 的顺序通常是四步走每一步都能独立上线、独立产生价值静态检查段代码风格、类型检查、敏感信息扫描。这一阶段最快通常十几秒到一分钟能挡掉大量低质量提交。测试段单元测试为主集成测试按需。这一阶段是最核心的也是耗时最长的部分。构建段编译打包产出可部署的产物。这一步如果不通过说明代码在真实构建条件下有问题。发布段把产物推到目标环境。这一步往往需要人工卡点不建议做成完全自动。一个常见的错误是把这四段塞进同一个 job 里顺序执行。看起来简单实际上有个大问题只要第一段失败后面全都不跑你拿不到任何额外信息而且失败重跑的时候前面的步骤要全部重来一遍。更好的做法是按关注点拆成多个 job让它们并行用needs控制依赖关系。这样 lint 挂了不影响测试结果你一次就能看到所有问题。2. 工作流文件里的每一行都在干什么配置写不对八成是因为没搞清几个核心概念之间的关系。这一节把层级关系和常用字段拆开讲理解了结构抄例子的时候就知道每行在改什么。2.1 workflow / job / step / runner 的层级关系用一句话概括层级一个 workflow 文件包含多个 job一个 job 包含多个 step每个 job 在一个 runner 上执行。workflow一个 YAML 文件就是一个 workflow放在.github/workflows/下文件名随意.yml或.yaml都行。job任务单元也是并行的基本粒度。不同 job 默认同时跑在不同的机器上各自拥有独立的工作目录。stepjob 内的执行步骤按顺序串行执行。上一步失败默认后面不跑除非用if改写。runner真正执行 job 的机器runs-on指定比如ubuntu-latest。这里有个新手最容易误解的点job 之间的文件系统是隔离的。你在 job A 里生成的文件job B 看不到除非你额外用 artifact 或者缓存机制传递。很多人把构建和部署拆成两个 job然后发现部署 job 找不到刚构建出来的包就是踩了这个坑。命令的工作目录也要注意。每个 job 的工作目录默认是仓库根目录actions/checkout会把代码拉到这个目录下。如果你在 step 里cd到子目录那只影响当前 step下一个 step 又回到根目录了。子目录构建要在每个 step 里都声明或者用working-directory字段统一指定。注意run步骤默认在 Linux runner 上用的是bash -e带-e但不带pipefail。这意味着cmd1 | cmd2这种管道只要cmd2成功就整体成功cmd1报错会被吞掉。真要在意的话把shell显式写成bash此时会启用-eo pipefail。2.2 on触发条件写得越精准账单越好看on决定了什么时候跑这是最影响成本的一个字段。最常见的写法有两种on: push: branches: [main, release/**] pull_request: branches: [main]只监听主干和发布分支的 push加上面向主干的 PR。别写成on: [push, pull_request]这种无脑全开的形式那样每个分支每推一次都跑PR 还会因为 push 和 pull_request 双触发跑两遍。pull_request和pull_request_target必须分清楚。前者的代码上下文是待合并的分支GITHUB_TOKEN在来自 fork 的 PR 上是只读的secrets 也不注入这是安全的默认行为。后者运行在目标分支的上下文里能拿到 secrets 和写权限设计初衷是处理打标签、评论这类操作。绝对不要在pull_request_target里检出并执行待合并分支的代码那等于把密钥交到外部提交者手上。路径过滤也很实用能用就一定要用on: push: paths: - services/api/** - .github/workflows/api-ci.yml后端服务的 CI 就没必要因为前端改了一个 CSS 文件而重跑。注意paths和paths-ignore不能同时写在同一个触发条件里。还有个几乎人人都该加的字段concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true同一个分支连续推好几次的时候把前面的运行取消掉只留最新的一次。做过实际项目就知道这个字段能省下多少时间尤其是那种习惯改一行推一次的同事。2.3 matrix一次提交跑遍多版本库类项目对多版本兼容性有硬要求一个个写 job 太蠢用矩阵jobs: test: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python-version: [3.10, 3.11, 3.12] os: [ubuntu-latest, macos-latest] exclude: - os: macos-latest python-version: 3.10 steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: ${{ matrix.python-version }} - run: pytest两个要点。第一fail-fast默认是true意思是矩阵里任何一个组合失败其他还没跑完的组合会被直接掐掉。调试阶段性看全貌的时候把它设成false一次拿到所有失败信息省得来回改代码重推。第二exclude用来剔除没意义的组合include用来追加特殊组合两个都能显著压低运行数量。矩阵展开的规模要算一下账。3 个语言版本 × 3 个操作系统 9 个 job每个 job 平均 3 分钟一次提交就是 27 分钟的机器时间。如果是私有仓库这是实打实的额度消耗。我的习惯是日常 PR 只跑主力版本加一个边界版本完整的全矩阵放到主干合并或者夜间任务里跑。2.4 缓存把重复下载的时间省下来没有任何优化的流水线时间基本都花在一件事上——重复下载依赖。pip 装一遍依赖动辄一两分钟npm 装一遍可能三分钟起步。缓存是投入产出比最高的一项优化。好消息是官方 action 大多内置了缓存支持最简单的用法- uses: actions/setup-pythonv5 with: python-version: 3.12 cache: pip- uses: actions/setup-nodev4 with: node-version: 20 cache: npm但这里有个非常经典的报错Error: No file in /home/runner/work/... matched to [**/requirements.txt ...]。原因是cache: pip需要它能找到一个依赖清单文件来算缓存键如果你的项目用的是pyproject.toml加poetry.lock或者依赖清单放在子目录里没被默认的通配规则匹配到它就会抱怨。解决办法是显式指定- uses: actions/setup-pythonv5 with: python-version: 3.12 cache: pip cache-dependency-path: backend/requirements/*.txt手动缓存更灵活也更适合复杂项目- uses: actions/cachev4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles(**/requirements.txt) }} restore-keys: | ${{ runner.os }}-pip-key是精确命中的依据restore-keys是降级匹配的依据。hashFiles算的是文件内容的哈希只要依赖没变key 就不变缓存就能复用依赖一改key 变了缓存自然失效从最近的旧缓存恢复一部分。这个设计挺巧妙的既保证正确性又不至于每次都从零开始。注意缓存是有作用域限制的。分支上的缓存默认只能被自己分支和默认分支的后续运行读到而且缓存总量有配额超过之后旧的会被淘汰。别把缓存当成持久化存储用构建产物该传 artifact 就传 artifact。3. 上手实操三套可以直接抄的配置原理讲完进入实操。这一节给三套配置覆盖后端、前端和容器化场景都是我在实际项目里跑过、并且删掉项目特有部分之后的通用版本。3.1 Python 项目lint 单测 覆盖率目标是提交即验证分两个 job 并行一个做静态检查一个跑测试。name: Python CI on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read jobs: lint: runs-on: ubuntu-latest timeout-minutes: 10 steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 cache: pip - name: Install lint tools run: pip install ruff0.6.9 - name: Ruff check run: ruff check . --output-formatgithub - name: Ruff format check run: ruff format --check . test: runs-on: ubuntu-latest timeout-minutes: 20 strategy: fail-fast: false matrix: python-version: [3.10, 3.12] services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: testpass POSTGRES_DB: testdb ports: - 5432:5432 options: - --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 env: DATABASE_URL: postgresql://postgres:testpasslocalhost:5432/testdb steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: ${{ matrix.python-version }} cache: pip - run: pip install -r requirements.txt -r requirements-dev.txt - run: pytest -q --covapp --cov-reportxml --junitxmlreport.xml - name: Upload test report if: always() uses: actions/upload-artifactv4 with: name: pytest-report-${{ matrix.python-version }} path: | report.xml coverage.xml几个设计意图值得说清楚。--output-formatgithub是给 lint 工具用的特殊参数它输出的告警会直接以行内注释的形式挂在 PR 的 diff 上点进去就能看到问题所在比翻日志高效得多。这个参数很多工具都支持用之前查一下自己的工具是否兼容。services那段起了一个带健康检查的 PostgreSQL。健康检查这行很重要没有它的话容器是起来了但数据库还没准备好接受连接你的测试会在启动时随机失败而且这种失败特别难查因为它时好时坏。加上--health-cmd之后GitHub 会等到健康检查通过才启动 job 的步骤。if: always()是让这个步骤无论前面成功失败都执行。测试失败的时候你要的就是那份报告如果它只在成功时才上传那最需要它的时候反而没有。timeout-minutes是防呆的。默认情况下一个 job 最长能跑 6 小时万一代码里有个死循环或者卡在等待资源上这个 job 会一直占着机器烧额度。给每个 job 设一个合理的上限比如测试 20 分钟、构建 30 分钟超时自动失败比手动去取消要省心。3.2 前端项目npm ci 与产物检查前端这边最容易踩的坑是安装方式的选择。npm ci和npm i差别很大在 CI 里应该无脑用前者。对比项npm cinpm i依据文件只认package-lock.json优先 lock缺失时解析package.json对node_modules先整体删除再全新安装增量更新是否改写 lock不改写可能改写安装速度通常更快视增量情况而定适用场景自动化流程、可复现构建本地开发新增依赖npm ci的确定性是它最大的价值只要 lock 文件不变任何机器上装出来的依赖树都是一样的。前提是lock 文件必须提交进仓库如果你的.gitignore里写了package-lock.json那这条路走不通先把它从忽略列表里删掉。name: Frontend CI on: pull_request: branches: [main] paths: - web/** - .github/workflows/frontend-ci.yml jobs: build: runs-on: ubuntu-latest timeout-minutes: 20 defaults: run: working-directory: web steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 cache: npm cache-dependency-path: web/package-lock.json - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm run build - uses: actions/upload-artifactv4 with: name: web-dist path: web/dist retention-days: 7defaults.run.working-directory是个省事的小技巧写了它之后job 里所有run步骤都自动在web目录下执行不用每行都写cd web 。注意它只管run步骤uses步骤的路径参数还得自己写全比如上面的cache-dependency-path。retention-days也值得配。产物默认保留 90 天实际上构建产物留一周足够了尤其是体积大的前端包一直堆着占存储配额。3.3 构建镜像与上传制品容器化项目的流水线通常长这样测试通过后构建镜像推到镜像仓库打上标签。这里用官方提供的几个 action 会比自己写docker build命令更省事。image: needs: [test] runs-on: ubuntu-latest timeout-minutes: 30 permissions: contents: read packages: write steps: - uses: actions/checkoutv4 - uses: docker/setup-buildx-actionv3 - name: Log in to registry uses: docker/login-actionv3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata id: meta uses: docker/metadata-actionv5 with: images: ghcr.io/${{ github.repository }} tags: | typeref,eventbranch typesha,prefixsha-,formatshort typeraw,valuelatest,enable{{is_default_branch}} - uses: docker/build-push-actionv6 with: context: . push: ${{ github.event_name ! pull_request }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: typegha cache-to: typegha,modemax这段配置里有三处值得展开。标签策略用一个 metadata action 自动生成比手写字符串拼接可靠得多。typeref,eventbranch让分支名成为标签typesha把提交短哈希打上去typeraw只在默认分支上打latest。这几个标签组合起来回滚的时候你可以精确定位到具体某次提交构建的镜像不至于只有一个latest根本不知道该回哪个。push 的条件判断是为了安全。来自 PR 的构建只验证能不能构建成功不推送。PR 上不应该有权限往镜像仓库写东西这一点用github.event_name判断最简单。构建缓存cache-from: typegha用的是 GitHub 自己的缓存后端专门给 BuildKit 用的。Docker 分层构建的缓存命中与否直接决定构建时间是 30 秒还是 5 分钟这个配置基本属于必加项。modemax表示连中间层的缓存也一并导出代价是缓存体积变大第一次构建会慢一点。3.4 密钥、变量与最小权限这一节讲讲容易出事的地方。权限配置的正确姿势是默认收紧按需放开。在 workflow 顶层写permissions: contents: read然后在具体需要额外权限的 job 里覆盖permissions: contents: read packages: write id-token: write不写permissions的话GITHUB_TOKEN会拿到仓库默认设置里的一整套权限包含写代码、改 issue、动 PR 等等。对一个只跑测试的 job 来说这些权限全是多余的一旦某条依赖被投毒或者某个 action 被劫持多余的权限就是攻击面。Secrets 的使用也有几条铁律。第一只在需要的地方引用不要图省事在 workflow 顶层设成 env 变量那样任何一步脚本都能读到。第二不要把 secrets 写进日志虽然平台会自动打码但只要你把它拼进字符串里输出打码就可能失效。第三给自己加一个防呆检查比如在脚本开头判断密钥是否为空为空就直接失败并提示免得带着空值去调接口报出一堆看不懂的错。- name: Check secret run: | if [ -z ${{ secrets.DEPLOY_KEY }} ]; then echo DEPLOY_KEY is not set exit 1 fi表达式语法里还有个细节${{ }}内部前后要有空格${{secrets.FOO}}这种写法在有些版本上解析会有问题。另外${{ }}是在 job 下发到 runner 之前就被替换掉的所以它不能用在run脚本的运行时判断里想动态取值得用env中转。4. 从 CI 走到 CD把部署这一步接上CI 跑顺了之后自然会想把部署也自动化掉。我的建议是谨慎推进部署这一步出错的代价比测试失败高一个数量级。4.1 制品怎么流转到部署环节前面提过 job 之间文件系统隔离构建和部署拆成两个 job 的话产物必须显式传递。推荐用 artifactbuild: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - run: ./build.sh - uses: actions/upload-artifactv4 with: name: app-bundle path: dist/ if-no-files-found: error deploy: needs: [build] runs-on: ubuntu-latest steps: - uses: actions/download-artifactv4 with: name: app-bundle path: dist/ - run: ./deploy.sh dist/if-no-files-found: error建议加上。默认行为是打个警告就算了结果就是构建产出了一个空目录部署 job 高高兴兴地部署了个寂寞线上发现问题再回头查代价就大了。这里有个 v4 版本的重要变化同名 artifact 不能重复上传。以前 v3 允许往同一个名字里追加文件v4 会直接报冲突。如果你的矩阵 job 都要上传报告名字里必须带上区分项比如report-${{ matrix.os }}-${{ matrix.python-version }}或者加overwrite: true让它覆盖。4.2 Environment 与人工卡点生产部署不该无条件自动执行。用 Environment 做一层保护deploy-prod: needs: [build] runs-on: ubuntu-latest environment: name: production url: https://example.com steps: - run: ./deploy.sh --env prod在仓库设置里给production这个 Environment 配置必须人工审批以及限定只有主干分支能进入。这样 job 跑到这一步会停住等指定的人点批准才继续。审批记录会留痕谁批的、什么时候批的都查得到。这比在群里喊一声我发了啊要正规得多。Environment 还可以挂独立的 secrets。测试环境的密钥和生产环境的密钥分开存各自只在自己的 Environment 里可见能避免很多误操作。4.3 定时任务与手动触发除了推代码触发另外两种触发方式也很常用。定时任务用schedule注意它用的是 UTC 时间而且只会在默认分支上生效on: schedule: - cron: 0 18 * * *上面这个写法是每天 UTC 18 点也就是北京时间凌晨 2 点。定时任务适合跑那种耗时长的全量测试、依赖安全扫描、性能基准对比。不要指望它精确到分钟高峰期排队延迟十几分钟是常事而且仓库长时间没有活动的话定时任务会被自动停用需要进仓库手动重新启用一次。手动触发用workflow_dispatch这个几乎每个 workflow 都该加on: workflow_dispatch: inputs: environment: description: Target environment required: true default: staging type: choice options: [staging, production] dry_run: description: Dry run only type: boolean default: true有了它你可以在网页上点按钮触发还能填参数。跑一次临时验证、手动重跑某个失败的任务都方便很多。type: boolean和type: choice会渲染成对应的控件比让用户手填字符串友好。5. 常见问题与排查实录CI 出错的类型其实很集中大部分都能归到有限的几类里。这一节把我和同事遇到过的典型问题整理成速查表后面再补几个反常识的细节。5.1 问题速查表现象大概率原因处理方式job 完全没被触发on的分支或路径过滤没命中检查分支名是否带refs/heads/前缀需求、路径 glob 写法报 No file matched 缓存错误缓存找不到依赖清单显式设置cache-dependency-pathfork 的 PR 里密钥为空安全机制fork PR 不注入 secrets改为合并后触发或用两段式流程缓存总是未命中key 里的文件哈希每次都在变检查是否把 lock 文件加进了 key是否误包含随机内容上传 artifact 冲突v4 不允许同名重复上传名字带矩阵变量或设overwrite: true步骤报 command not found上一步cd只影响当前步骤用working-directory或每步都 cd明明测试挂了但 job 绿了管道吞了退出码显式shell: bash或加set -o pipefail运行时间忽长忽短依赖下载或 runner 调度波动加缓存、设超时、避开整点高峰分支保护要求的状态检查找不到检查名和 job 名不匹配把 job 的name显式写清楚并统一步骤卡住不结束有交互式提示或死循环给命令加非交互参数设timeout-minutes这张表里我觉得最值得说道的是测试挂了但 job 是绿的。曾经有个项目用pytest | tee test.log这种写法容器里默认的 shell 没有开pipefailtee永远返回 0于是无论测试怎么挂整体退出码都是 0流水线常年绿色。这个问题藏了小半年才被发现因为大家习惯了看绿勾就合代码。从那之后我给自己定了条规矩任何带管道的命令都要确认退出码传递是否正确。5.2 几个反常识的坑YAML 里的on可能被解析成布尔值。在某些第三方 YAML 解析器里on会被当成true处理这是 YAML 1.1 规范的历史遗留。GitHub 自己的解析器处理得了但如果你用脚本去读这个文件就要注意加引号写成on。缩进必须用空格且不能错位。YAML 不允许 tab混用会直接解析失败。错误信息通常是mapping values are not allowed in this context位置提示还常常指偏实际错误可能在前几行。我的做法是本地装个 YAML 插件或者提交前用actionlint跑一遍它专门针对工作流文件做校验能提前抓出很多问题。if条件里不需要写${{ }}。在if字段里直接写github.event_name push就行写了${{ }}能跑但是多余而且在某些位置会引发语法错误。needs只控制顺序不共享环境。needs: [build]的意思是等 build 成功后再跑当前 job它不会把 build 的文件带过来产物传递还得靠 artifact。跳过 job 不等于成功。如果上游 job 被条件判断跳过了下游带needs的 job 默认也会被跳过状态显示为 skipped 而不是 success。如果分支保护里要求那个检查必须通过skipped 会被判定为未满足条件PR 就一直合不了。解决办法是在下游 job 的if里加always()并自行判断上游结果。矩阵里的变量名大小写敏感。matrix.python-version和matrix.python_version是两个不同的东西一个下划线一个连字符写错了不会报错只会得到空值。5.3 权限与安全上的红线自动化流程一旦有了写权限它就成了攻击面。我给自己定了几条不越线的规矩。第三方 action 要固定版本。用uses: some/actionv3这种方式标签是可以被移动的万一上游账号被盗攻击者可以把v3指向恶意提交你的流水线下次运行就会执行它。更稳妥的做法是固定到完整 commit SHA虽然升级麻烦一点但安全性高得多。至少也要用官方的、star 数多的 action。不要在pull_request_target里执行不可信代码。前面提过这里再强调一次。这个触发事件的设计目的是做仓库治理类操作不是给外部提交跑代码的。在这个上下文里检出并运行 PR 分支的脚本等于把 secrets 和写权限送出去。GITHUB_TOKEN要按 job 最小化。默认权限在组织层面可以设置但更可靠的是在每个 workflow 里显式声明。只读的 job 就写contents: read别的全不给。日志里不要打印敏感信息。就算平台有自动打码也不要依赖它。调试的时候临时echo一下密钥忘了删的情况太常见了。我的习惯是写个mask步骤或者干脆在提交前 grep 一遍 workflow 目录里有没有可疑的echo $。自建 runner 不要给公开仓库用。公开仓库意味着任何人都能提交 PR如果你把自建 runner 挂在上面且给了敏感权限风险不言而喻。要用的话加审批流程或者干脆只跑在私有仓库和内部网络上。5.4 让流水线长期保持健康的几个习惯配置写完不是结束而是开始。时间长了会积累一堆问题跑得越来越慢、偶尔随机失败、没人记得某段配置是干嘛的。我自己维持的几个习惯效果还不错。给每个 job 取人能看懂的名字。name: 单元测试 (Python 3.12)比test-2直观得多分支保护里勾选的状态检查也是按这个名字显示的名字清晰能省掉很多沟通成本。把耗时打印出来。默认界面会显示每步耗时但我想看的是相对上次变慢了多少。简单的做法是让脚本自己输出关键阶段的时间戳日志里一搜就能定位是哪一步在变慢。随机失败的用例要当 bug 处理。测试偶发失败然后重试通过这种做法迟早会掩盖真实问题。遇到就记下来专门花时间查多半是并发、时序或者共享状态的锅。定期清理废弃的 workflow。禁用的、临时性的、早就没用的流水线文件留在仓库里会让新人困惑。不用就删需要的时候从历史里找回来。把仓库设置里的默认 token 权限收窄。在组织或仓库的安全设置里把默认的GITHUB_TOKEN权限设为只读需要写权限的 workflow 自己显式声明。这一步一次设置长期受益。我自己踩过印象最深的一次是缓存 key 写错导致构建时间翻了三倍查了两天才发现是因为 key 里误带了一个每次都在变的变量缓存永远命中不了所有依赖每次全量重装。从那以后我养成了一个习惯新加的缓存配置第一件事就是看两次运行的日志里缓存有没有命中日志里会明确写Cache restored from key或者Cache not found一眼就能确认。这个习惯帮我省下了不少排查时间。另外值得一提的是别把 CI 配置写得过于聪明。见过有人在 workflow 里塞了复杂的条件判断和动态生成的步骤结果没人敢改。流水线配置的第一要求是看得懂第二才是高效。宁可多拆两个 job、多写几行重复的 YAML也别搞那种只有作者本人能维护的魔法配置。
返回列表