news 2026/10/31 14:38:20

EAS Workflows 实战指南:从 0 到 1 搭建 Expo 应用商店发布 CI/CD 流水线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
EAS Workflows 实战指南:从 0 到 1 搭建 Expo 应用商店发布 CI/CD 流水线

EAS Workflows 实战指南:从 0 到 1 搭建 Expo 应用商店发布 CI/CD 流水线

【免费下载链接】skillsA collection of AI agent skills for working with Expo projects and Expo Application Services项目地址: https://gitcode.com/GitHub_Trending/skills9/skills

如果每次发版你还靠在本地手敲eas build和eas submit,这套流程慢、难复现、也很难交接给同事。本文以 EAS Workflows 为主线,从 0 到 1 带出一套可版本化、可复现的 Expo 应用商店发布 CI/CD 流水线:先跑通推送即构建,再叠加 PR 预览与双平台生产发布,最后用服务端校验和凭据托管收尾,读完后你可以独立写出并验证一条面向 App Store / Google Play 的发布流水线。

EAS Workflows 能自动化什么、不能自动化什么:先定边界

写 YAML 之前先把三条边界钉死,比背字段省心得多。主技能文件把它的定位说得很干脆:EAS Workflows 是 "the build → submit → update pipeline" 的自动化层,见 eas-app-stores/SKILL.md。

它是编排层,不是新的构建系统。type: build作业仍然调用 EAS Build,type: submit仍然调用 EAS Submit,profile字段指向eas.json里已存在的构建/提交配置。工作流只负责三件事:顺序、触发条件、并行关系。

它不包办所有场景。workflows.md 开头就划好了分工:需要写、改或校验超出示例范围的 workflow YAML 时改用eas-workflows技能;网站与 API 路由的部署工作流(type: deploy)归eas-hosting技能管。

"成功"只到 EAS 服务为止。技能原文有一句值得逐字记住:

A successful EAS build or a queued submission does not establish Apple acceptance, tester access, or an App Store release. (EAS 构建成功或提交已排队,都不代表 Apple 已接受、测试者可获得访问、或 App Store 发布生效。)

这句话写 YAML 时同样成立:submit作业变绿,只说明上传被排队了。这一点后文"别把作业成功当已上架"会展开。

计费与账号前提(触发前先看一眼)

EAS Workflows 跑在 Expo Application Services 上,属于计费产品:

  • 每个作业消耗所属计划的 build/compute minutes;
  • 执行构建或提交的作业,还需要付费的 Apple Developer 与 Google Play 开发者账号;
  • 触发前建议先确认所请求服务的免费额度与付费限制。

维护这个仓库的同学还会注意到,scripts/check-skill-limits.ts 会在 CI 中强制校验所有eas-*技能在 H1 之后紧接成本声明——所以每个eas-开头的技能正文都以成本提示开场。这是仓库约束,不是写作风格。

另外,工作流跑在 EAS 服务端,读不到你本地的隐式状态。按 expo-overview 的共享约定,两个"连接状态"必须提前就位:

  • 登录态:用eas whoami检查、eas login建立;
  • 项目已链接:应用配置中存在extra.eas.projectId;缺失时用eas init创建。

动笔前固定的格式底线

以下是纯机械性约束,违反了连语法校验都过不去,快速过一遍即可。

文件位置与体积:工作流放在.eas/workflows/*.yml(或.yaml),每个文件不得超过 16 KiB。仓库示例都遵循这一约定,例如 ios-app-store.md 与 app-store-metadata.md 中的片段均标注为.eas/workflows/release.yml。

顶层键一共五个:

顶层键作用是否必需
name工作流展示名称示例均提供
on触发条件至少一个
jobs作业定义必需
defaults所有作业共享的默认值可选
concurrency控制并行运行可选

动态值统一用${{ }}语法,schema 定义了github.*、inputs.*、needs.*、jobs.*、steps.*、workflow.*六族上下文。本文四个发布场景实际只用到其中三种:${{ github.event.pull_request.number }}、needs.<job>、needs.<job>.outputs.*。

eas-workflows技能反复提醒:每个分区的完整规范应以 workflow schema 为准,别凭记忆填字段。

第一步:先跑通"推送即构建"

别用复杂场景开局。最简单的起点是"推送到指定分支就构建":

# .eas/workflows/build.yml name: Build on: push: branches: - main - release/* jobs: build: type: build params: platform: all profile: production

出处见 workflows.md。这里有两个新值:

  • platform: all—— 一个作业覆盖双平台,代价是失去按平台单独重试、单独诊断的粒度(生产发布再讲拆分写法)。
  • release/*通配—— 所有发布分支共享同一条流水线,不必逐个枚举。

这个示例刻意不带submit:典型用途是"每次合并主干都产出一个可安装验证的产物",把提交动作留给人工决策或后面的 tag 流水线。

跑通这一步后先验证两件事:eas.json里的productionprofile 能被作业解析、产物确实能下载。这两点通过后,再往复杂场景走。

第二步:加一条 PR 预览分支——为什么是 type: update

同样是"PR 预览",Web 与原生是两条完全不同的路,别混用:

  • 网站与 API 路由走type: deploy搭配prod: false,目标是 EAS Hosting(Cloudflare Workers),见 eas-hosting/SKILL.md;
  • 原生 App 走type: update,OTA 发布 JS bundle 与资源,不产生新的原生二进制。

PR 预览示例:

# .eas/workflows/pr-preview.yml name: PR Preview on: pull_request: types: [opened, synchronize] jobs: publish: type: update params: branch: "pr-${{ github.event.pull_request.number }}" message: "PR #${{ github.event.pull_request.number }}"

为什么 update 就够用:PR 预览改的通常是 JS 与样式,正好落在 OTA 能力范围内。eas-update 技能给出的模型是——build是装到设备上的原生 App,update是面向某平台与 runtime version 的 JS/资源包,branch是有序的更新流,channel是构建时嵌入的部署目标。一旦改动涉及原生代码或原生配置,OTA 就兜不住了,必须重新构建。这也解释了为什么下节的生产发布用build+submit而不是update。

一行值得抄的写法:branch: "pr-${{ github.event.pull_request.number }}"让每个 PR 拥有独立更新分支、互不覆盖;message里带上 PR 号,之后在 EAS 面板排查"这个预览包来自哪个 PR"时能直接对上。

这里有个容易踩的坑:预览构建的 channel 必须指向这条分支,更新才能下发。eas-update 的判断条件是"平台匹配、runtime version 匹配、且已安装构建的 channel 指向包含该更新的 branch"三者同时成立。channel 与 branch 虽然常常同名,但在服务端是两个独立对象——eas channel:edit改的是 channel 的服务端分支映射,不会改变已安装构建里嵌入的 channel。所以 PR 预览方案要单独规划预览构建的 channel/分支映射策略,别指望pr-123这个分支名自动生效。

第三步:生产发布——双平台并行构建与提交

结构最完整的场景,"Complete release workflow for both platforms":

# .eas/workflows/release.yml name: Release on: push: tags: ['v*'] jobs: build-ios: type: build params: platform: ios profile: production build-android: type: build params: platform: android profile: production submit-ios: type: submit needs: [build-ios] params: platform: ios profile: production submit-android: type: submit needs: [build-android] params: platform: android profile: production

依赖图怎么读

四个作业构成两条互不相交的链:build-ios → submit-ios、build-android → submit-android。链之间没有needs关系,所以 iOS 与 Android 并行推进,每个submit只等自己的build。这比"先构建两个平台、再统一提交"的串行写法快,也避免单平台失败干扰另一条链的诊断。

profile 是引用,不是定义

profile: production必须在项目eas.json中存在对应条目,否则作业启动时才失败。注意一个语义上的"惊喜":build.production与submit.production是两个不同分区下的同名 profile。示例不区分分区地写profile,是因为type已经决定了查哪张表——type: build读build,type: submit读submit。主技能文件里的完整示例:

{ "build": { "production": { "autoIncrement": true, "ios": { "resourceClass": "m-medium" } } }, "submit": { "production": { "ios": { "appleId": "your@email.com", "ascAppId": "1234567890" }, "android": { "serviceAccountKeyPath": "./google-service-account.json", "track": "internal" } } } }

关键改进:显式传 build_id

上面示例的 submit 作业只传了platform和profile,默认会去选"最新构建"来提交。在每天都触发构建的项目里,"最新构建"很可能不是你刚构建出来的那一个。ios-app-store.md 给了更严谨的写法:

submit: type: submit needs: [build] params: build_id: ${{ needs.build.outputs.build_id }} profile: production

testflight.md 的理由很直接:"For separate jobs, pass the explicit build ID rather than selecting whichever unrelated build is newest."(拆分的作业之间,要显式传构建 ID,而不是选最新的那个无关构建。)用needs.build.outputs.build_id把两者绑定,可以防止把错误二进制推到商店;发布流程有可审计性要求的话,建议直接把build_id写进 submit 作业。

触发方式:用 tag 驱动版本化发布

on.push.tags: ['v*']让流水线只在打标签时运行,发布点与代码版本一一对应,出问题时能直接 checkout 对应 commit 复查。参考文档 Tips 建议两种触发搭配:tag 做版本化发布,workflow_dispatch处理手动生产发布。

第四步:按需触发——条件作业

前两条流水线都是"无条件"触发,README 改一个字也消耗一次构建额度。加一层探测来过滤:

# .eas/workflows/conditional-release.yml name: Conditional Release on: push: branches: [main] jobs: check-changes: type: run params: command: | if git diff --name-only HEAD~1 | grep -q "^src/"; then echo "has_changes=true" >> $GITHUB_OUTPUT fi build: type: build needs: [check-changes] if: needs.check-changes.outputs.has_changes == 'true' params: platform: all profile: production

三段结构一目了然:type: run作业执行 shell 命令做探测;needs: [check-changes]声明依赖;if决定是否真正开工。这是"只在源码目录变化时才发布"的标准写法——文档、README、CI 配置的改动不会白白消耗构建额度。

两个细节:

  1. if的比较用字符串'true'而非布尔true,与其上游$GITHUB_OUTPUT的文本性质一致;
  2. eas-workflows 的生成清单要求"确保if条件符合 schema 的长度约束",说明该字段并非无限制的自由文本。

⚠️ 这里有一处需要自行验证的细节:check-changes用 GitHub Actions 约定$GITHUB_OUTPUT写出结果,下游读的是 EAS 表达式上下文needs.check-changes.outputs.has_changes。这两者是否在同一次运行中直接打通,取决于run作业类型对输出的实际处理方式——仓库文档没有进一步说明,而eas-workflows反复强调"不要依赖记忆中的字段值,先取 schema 再写"。建议照搬进生产前按这个顺序做:

  1. 取 schema,确认run作业的输出声明方式与needs.*.outputs的可用性;
  2. 用下一节的workflow:validate做服务端校验;
  3. 在一条非生产分支上跑一次,确认has_changes取值符合预期,再接入main。

写完必做的一步:服务端 workflow:validate 校验

四条 YAML 写完,必须落到一个能给出确定性结论的动作上。eas-workflows技能给的命令:

npx -y eas-cli@latest workflow:validate .eas/workflows/<workflow.yml> --non-interactive

围绕这条命令有几条约束值得展开:

  • 逐个文件运行:每个被修改的工作流都单独执行一次校验;
  • 需要登录态与已链接项目:它不是纯离线工具;
  • 会检查 profile 引用:与纯 schema 校验不同,它还会把profile与项目的eas.json对照——正好能拦下"profile: production写错或不存在"这类错误;
  • 会做服务端校验:在这里修复成本远低于上线后发现;
  • 成功标志是固定文案:命令打印Workflow configuration YAML is valid.才算通过;反复修复并重跑,直到出现该输出;
  • 明确禁止替代:文档直接写明 "Do not replace this command with a local YAML or JSON Schema validator."

为什么强调"取实时 schema"

同样的谨慎也体现在资料获取上。写之前,eas-workflows技能要求先拉取三份资源:

  1. JSON Schema—— 文档用大写强调 "It is NECESSARY to fetch this schema"。它是作业类型、参数、触发类型、runner 类型的权威来源(最终校验仍以 EAS CLI 为准);
  2. 语法文档—— YAML 概览、示例、表达式与上下文;
  3. 预打包作业文档—— 各 job type 的参数与输出。

为避免 Agent 反复抓取被限流,技能自带一个缓存抓取脚本 scripts/fetch.js:按 URL 哈希落缓存、带 ETag 条件请求支持 304 复用、无缓存头时兜底 15 分钟 TTL。正因为 schema 的"新鲜度"由服务端缓存头控制,"每次写 YAML 前都取 schema"在实践中才是可行的。

五项自查清单

同一份技能还给出写作阶段的自查,可直接当 review checklist:

  1. 先取 schema,拿到当前 job type、参数与允许值;
  2. 核对每种 job type 的必填字段是否齐全;
  3. 确认needs与after中引用的作业确实存在于本文件——删掉一个 job 却忘了清理别处的needs,是这类 YAML 最常见的翻车点;
  4. 检查表达式引用的上下文与输出是否有效;
  5. 确认if条件符合 schema 的长度约束。

别把"作业成功"当成"已上架"

工作流只编排作业,真正的商店行为由type: submit背后的 EAS Submit 决定。要让流水线可靠,得理解下游两个特性。

提交只是"排队"

主技能文件列出提交相关的只读命令,并标注了版本差异:

# 列表与查看提交作业 eas submit:list -p ios --json eas submit:view SUBMISSION_ID --json

这些命令在 EAS CLI 23.2.0 中可用,但在测试过的 18.6.0 中不存在;不想改全局安装时,可以用npx eas-cli@23.2.0这类固定版本调用。testflight.md 进一步区分了两个命令的语义:submit:view报告的是EAS 作业,submit:status才读取 App Store Connect,反映 App Store 版本与 TestFlight 处理/内测/外测状态,并且需要对应 profile 或环境中的 App Store Connect API key。

同文件的"状态推进表"把"验证到了哪一步"的边界说得很清楚:

已验证状态它证明了什么下一步检查
EAS 构建完成产出了产物确认 build ID、来源提交与 profile
提交已排队EAS 安排了上传跟踪 submission URL 与 worker 日志
Apple 上传/处理成功Apple 接受了二进制TestFlight 可用性与合规事项
分配给测试者并可用目标测试者可安装在真机安装验证
App Review 批准并发布公开 App Store 分发仅属于已授权的生产发布

实际含义是:不要在submit作业成功后立即宣布"已上架"。文档还特别提醒,不能仅凭--no-wait返回成功就认定上传完成。

元数据推送:顺序里有业务含义

参考示例以单作业两步的形式给出:

# .eas/workflows/release.yml jobs: submit-and-metadata: steps: - name: Submit to App Store run: eas submit -p ios --latest - name: Push Metadata run: eas metadata:push

见 app-store-metadata.md。注意这种写法与前面"作业类型 +params"的风格不同——它依赖作业内的steps列表,而steps.*正是可用表达式上下文之一,所以这种写法在 schema 中有对应位置。该片段本身未声明作业类型,实际接入时以 schema 与workflow:validate的结果为准。

两个 step 的先后不能随意调换:metadata:push之前必须先有二进制。元数据文档的常见问题里就有一条 "Binary not found",解法是 "Push a binary witheas submitbefore pushing metadata"。

Android 侧:同一份 YAML 服务不同发布策略

type: submit在 Android 上落到 Play Console 的某个 track(internal→alpha→beta→production),由eas.json中submitprofile 的track与releaseStatus控制:暂停进行中的发布用releaseStatus: "halted";做灰度用inProgress搭配rollout: 0.1(先放 10%)。这意味着:同一份工作流 YAML 可以为不同的发布策略服务,只需在eas.json里换 track 与 rollout 参数,不必改 YAML。

还有一条容易忽略的边界:submit:status目前只读取 Apple 的状态,EAS 作业成功并不能证明 Play Console 上的发布已生效(见 play-store.md)。排查 Android 发布时,去 Play Console 看 track 与发布状态。

凭据别进仓库

参考文档 Tips 的最后一条:"Keep sensitive values in EAS Secrets, not workflow files." 工作流文件会进 Git 仓库,任何写死的凭据都会随之扩散。

Android 服务账号是最典型的需要处理的凭据。它可以以文件形式存进 EAS Secrets,再被eas.json引用:

eas secret:create --name GOOGLE_SERVICE_ACCOUNT \ --value "$(cat google-service-account.json)" --type file
{ "submit": { "production": { "android": { "serviceAccountKeyPath": "@secret:GOOGLE_SERVICE_ACCOUNT" } } } }

见 play-store.md。注意serviceAccountKeyPath里放的是@secret:引用而非真实路径——这样工作流 YAML 与eas.json都可以安全提交,密钥只在 EAS 服务端解析。CI 环境下的另一种做法是EXPO_ANDROID_SERVICE_ACCOUNT_KEY_BASE64环境变量(服务账号 JSON 的 base64)。

排错速查表

把参考文档 Tips 与各参考文件中的故障条目合并,按"先查什么"组织:

现象先查什么出处
工作流 YAML 校验不通过用workflow:validate逐个文件重跑,确认profile存在于eas.jsoneas-workflows/SKILL.md
作业引用的前置作业不存在检查needs/after是否指向本文件中真实存在的 jobeas-workflows/SKILL.md
PR 预览更新下发不到设备核对构建的 channel 是否指向pr-<n>分支、平台与 runtime version 是否匹配eas-update/SKILL.md
提交推到了错误的构建改用build_id: ${{ needs.build.outputs.build_id }}显式绑定testflight.md
submit成功但无可安装包查 TestFlight 处理/合规状态与测试组分配testflight.md
元数据推送报 "Binary not found"先跑一次eas submit,再eas metadata:pushapp-store-metadata.md
Android 版本号冲突autoIncrement: true,配合appVersionSource: "remote"play-store.md
iOS 上传被拒(大图标 alpha 通道)移除默认 AppIcon PNG 的 alpha 通道后重新构建testflight.md
Android 提交报权限不足在 Play Console → API access 给服务账号 "Release to production"play-store.md

主技能文件还写了一条通用汇报纪律:报告时给出确切的 build ID / 版本号,以及已验证到的最远状态,不要把"作业成功"等同于"商店可下载"。

落地六步清单

第一次接入工作流的项目,建议按这个顺序推进,每步都有明确的验证动作:

  1. 对齐账号状态:eas whoami确认登录,检查应用配置中的extra.eas.projectId,缺失则eas init;
  2. 先写非生产流水线:从"推送到 main 即构建"开始,只跑type: build,确认profile能被解析、产物能下载;
  3. 加上校验环节:把npx -y eas-cli@latest workflow:validate .eas/workflows/<file>.yml --non-interactive作为提交前的固定动作;
  4. 接入 PR 预览:先确认预览构建的 channel/分支映射策略,再打开pull_request触发,避免一开就产生大量无效分支;
  5. 最后接生产发布:tag 触发 + 显式build_id的 submit 作业,并在流水线之外保留 TestFlight / Play Console 的人工核验;
  6. 把凭据移出仓库:所有服务账号、API key 走 EAS Secrets,YAML 与eas.json只保留引用。

收尾:记住的是四个技能的边界

eas-app-stores负责"构建什么、提交到哪",eas-workflows负责"YAML 怎么写、怎么校验",eas-update负责"OTA 的通道与兼容性",eas-hosting负责"网页与 API 路由的部署"。把这四个技能的边界分清,比记住任何一个字段名都更有长期价值。

【免费下载链接】skillsA collection of AI agent skills for working with Expo projects and Expo Application Services项目地址: https://gitcode.com/GitHub_Trending/skills9/skills

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 14:37:09

将一块数据盘扩容到/目录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/31 14:23:37

基于AT89S52单片机的蘑菇大棚环境监测系统论文(附录代码)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/31 14:23:21

【优化求解】基于无序灰狼算法求解多目标问题matlab代码

1 简介Mirjalili 等人提出了一种新的群体智能算法———灰狼优化算法(GWO)&#xff0c;并通过多个基准测试函数进行测试&#xff0c;从结果上验证了该算法的可行性&#xff0c;通过对比&#xff0c;GWO 算法已被证明在算法对函数求解精度和稳定性上要明显优于 PSO、DE 和 GSA 算…

作者头像 李华
网站建设 2026/10/31 14:23:02

STEP 7 V5.x 老项目升级:HSP 硬件支持包安装与排查实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/31 14:23:02

振镜装好没反应?五步配置法从接线到出光排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华