{
  "id": "expo-eas-update",
  "type": "loadout",
  "title": "Expo EAS Update without shipping native changes over the air",
  "summary": "Checks whether an Expo change can ship as an EAS Update or needs a new build, using the native fingerprint; preview by CI, production rollouts by a person.",
  "description": "An EAS Update swaps the JavaScript inside a build, but it cannot add native code. An agent that installs expo-camera and then runs eas update ships a crash to every user on that build. This loadout gives your agent a check it runs before any update. It records the native fingerprint when a store build is cut, then compares the current project against it with expo-updates' own fingerprint tool. JavaScript and version bumps pass. A new native module, config plugin or permission fails, and the output names what changed. A config check makes sure runtimeVersion uses the fingerprint policy, updates.url matches the project and every build profile has its own channel. It validates eas.json with Expo's own parser. CI publishes to preview only. Production goes out as a staged rollout run by a person. The verifier runs it on Expo SDK 58 with expo-updates 58.0.17.\n",
  "highlights": [
    "OTA-safe or needs a new build, from the expo-updates native fingerprint",
    "Names what changed: added native modules, config, autolinking",
    "Fingerprint runtime policy, matching updates.url, one channel per build profile",
    "CI publishes to preview only; production is a person's staged rollout",
    "Verified on Expo SDK 58, expo-updates 58.0.17 and @expo/eas-json 25.1.1"
  ],
  "version": "1.0.0",
  "license": "MIT",
  "author": {
    "name": "Agent Loadout",
    "url": "https://loadout.jnw-it.com"
  },
  "credits": [
    {
      "title": "Expo docs: runtime versions and updates",
      "url": "https://docs.expo.dev/eas-update/runtime-versions/"
    },
    {
      "title": "Expo docs: EAS Update rollouts",
      "url": "https://docs.expo.dev/eas-update/rollouts/"
    },
    {
      "title": "Expo docs: fingerprint",
      "url": "https://docs.expo.dev/versions/latest/sdk/fingerprint/"
    },
    {
      "title": "Expo docs: configure EAS Build with eas.json",
      "url": "https://docs.expo.dev/build/eas-json/"
    },
    {
      "title": "expo/eas-cli",
      "url": "https://github.com/expo/eas-cli"
    }
  ],
  "categories": [
    "web-app",
    "workflow"
  ],
  "stacks": [
    "expo",
    "react-native",
    "typescript",
    "github-actions"
  ],
  "files": [
    {
      "path": "AGENTS.md",
      "role": "instructions",
      "description": "Rules for an Expo app updated over the air with EAS Update, where native changes always get a new build.",
      "skill": null,
      "agent": null,
      "install_to": {
        "claude-code": "AGENTS.md",
        "codex": "AGENTS.md",
        "cursor": "AGENTS.md",
        "gemini-cli": "AGENTS.md",
        "antigravity-cli": "AGENTS.md",
        "copilot": "AGENTS.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/AGENTS.md",
      "bytes": 1558,
      "lang": "markdown",
      "content": "# AGENTS.md: Expo app shipped with EAS Update\n\nAn Expo SDK 58 app (React Native 0.88) built with EAS Build and updated over the air with EAS Update. Check\n`package.json` for exact versions, and add packages with `npx expo install` so versions match the SDK.\n\n## Commands\n- Typecheck: `npx tsc --noEmit`.\n- Bundle check: `npx expo export --platform ios --platform android --output-dir dist`.\n- Update config check: `node .agents/skills/eas-update-release/scripts/ota_check.mjs config`.\n- OTA safety: `node .agents/skills/eas-update-release/scripts/ota_check.mjs compare` (against the fingerprint\n  recorded when the current store build was cut).\n\n## Rules\n- Use the `eas-update-release` skill before any change is published as an update, and after adding or upgrading\n  a package or editing app.json, app.config or eas.json.\n- `runtimeVersion` uses the fingerprint policy. If `compare` reports a native change, the change needs a new\n  build: say so, do not publish an update.\n- Agents may publish to the `preview` channel through CI. Production updates, rollouts and rollbacks are run by\n  a person.\n- Never run `eas update --auto`, `--force-end-active-rollout`, `eas update:rollback` or `eas channel:*`.\n- Never edit or regenerate the committed build fingerprint to make a check pass. It is recorded when a build is cut.\n- `EXPO_TOKEN` and other secrets live in CI secrets or EAS environment variables, never in the repository.\n- Code that depends on a new native module checks it exists before calling it, so an older build degrades\n  instead of crashing.\n"
    },
    {
      "path": "CLAUDE.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Claude Code.",
      "skill": null,
      "agent": "claude-code",
      "install_to": {
        "claude-code": "CLAUDE.md",
        "codex": null,
        "cursor": null,
        "gemini-cli": null,
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/CLAUDE.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/CLAUDE.md",
      "bytes": 415,
      "lang": "markdown",
      "content": "# CLAUDE.md\n\n@AGENTS.md\n\n## Claude Code notes\n\n- Skills from this loadout live in `.claude/skills/`: `eas-update-release`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Expo EAS Update without shipping native changes over the air\" v1.0.0 — https://loadout.jnw-it.com/loadouts/expo-eas-update/ -->\n"
    },
    {
      "path": "GEMINI.md",
      "role": "adapter",
      "description": "Imports AGENTS.md for Gemini CLI.",
      "skill": null,
      "agent": "gemini-cli",
      "install_to": {
        "claude-code": null,
        "codex": null,
        "cursor": null,
        "gemini-cli": "GEMINI.md",
        "antigravity-cli": null,
        "copilot": null
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/GEMINI.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/GEMINI.md",
      "bytes": 416,
      "lang": "markdown",
      "content": "# GEMINI.md\n\n@./AGENTS.md\n\n## Gemini CLI notes\n\n- Skills from this loadout live in `.agents/skills/`: `eas-update-release`. Use them when their description matches the task.\n- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.\n\n<!-- From the Agent Loadout \"Expo EAS Update without shipping native changes over the air\" v1.0.0 — https://loadout.jnw-it.com/loadouts/expo-eas-update/ -->\n"
    },
    {
      "path": "skills/eas-update-release/SKILL.md",
      "role": "skill",
      "description": "Config, the OTA-safe check, and who publishes where.",
      "skill": "eas-update-release",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/eas-update-release/SKILL.md",
        "codex": ".agents/skills/eas-update-release/SKILL.md",
        "cursor": ".agents/skills/eas-update-release/SKILL.md",
        "gemini-cli": ".agents/skills/eas-update-release/SKILL.md",
        "antigravity-cli": ".agents/skills/eas-update-release/SKILL.md",
        "copilot": ".agents/skills/eas-update-release/SKILL.md"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/skills/eas-update-release/SKILL.md",
      "bytes": 4001,
      "lang": "markdown",
      "content": "---\nname: eas-update-release\ndescription: Decide whether an Expo change can ship as an EAS Update or needs a new store build, check the update config, and prepare a staged rollout. Use before publishing with eas update, after adding or upgrading a package, or when editing app.json, app.config or eas.json.\n---\n\n# Shipping with EAS Update\n\nAn EAS Update replaces the JavaScript and assets inside a build. It cannot change native code. If an update\ncalls a native module the build does not contain, it crashes on users' phones, and `expo-updates` falls back\nto the previous update only if it notices. This skill keeps that from happening.\n\n## 1. Config once: fingerprint runtime version and one channel per profile\n\n```bash\nnode .agents/skills/eas-update-release/scripts/ota_check.mjs config\n```\n\nIt reads the resolved app config (`npx expo config --type public`) and checks:\n\n- `runtimeVersion` is `{ \"policy\": \"fingerprint\" }` (top level or per platform). The fingerprint changes\n  whenever anything that affects the native layer changes. A manual string or `appVersion` policy only works\n  if someone remembers to bump it, so the script notes it.\n- `updates.url` is `https://u.expo.dev/<projectId>` and matches `extra.eas.projectId`.\n- `eas.json` parses with Expo's own validator (`@expo/eas-json`), and every non-development build profile has\n  its own `channel`, including one called `production`.\n\nStart from `references/app.json` and `references/eas.json`.\n\n## 2. Every change: is it OTA-safe?\n\nWhen a store build is cut, record its native fingerprint and commit the file:\n\n```bash\nnode .agents/skills/eas-update-release/scripts/ota_check.mjs record      # writes .eas/build-fingerprint.json\n```\n\nBefore any update, compare:\n\n```bash\nnode .agents/skills/eas-update-release/scripts/ota_check.mjs compare\n```\n\n- `OTA-safe` (exit 0): only JavaScript, assets or non-native config changed. An update can carry it.\n- `needs a new build` (exit 1): it lists what changed, for example `added: node_modules/expo-camera/package.json`\n  or `changed: expoConfig`. Do not publish an update. Say a new build is needed and why.\n\nTypical \"needs a build\" changes: adding or upgrading any package with native code (`npx expo install` of an\n`expo-*` module), a new config plugin, permissions, icons and splash, bundle identifiers, an SDK upgrade.\nA version bump alone does not change the fingerprint.\n\nWith an Expo account the same check is `eas fingerprint:compare --build-id <id>`. The script is for CI and\nfor machines without a token.\n\n## 3. Publishing: preview by the pipeline, production by a person\n\n- Preview: CI publishes to the `preview` channel when `compare` passes. See\n  `references/eas-update-preview.yml`. `--environment` is required for SDK 55 and later.\n- Production, done by a person after testing on a preview build, as a staged rollout:\n\n  ```bash\n  eas update:republish --branch preview --destination-channel production --rollout-percentage 10 -m \"...\"\n  eas update:edit                      # raise the percentage once crash-free rates hold\n  eas update:revert-update-rollout     # or back out\n  ```\n\n- One rollout per branch and runtime at a time. Never pass `--force-end-active-rollout` to get past one.\n- Roll back with `eas update:rollback`. A person runs it; it affects every user on that runtime.\n\n## What the agent does not do\n\n- No `eas update` to the `production` channel or branch, no `--auto` (it publishes to a branch named after\n  the git branch), no `update:rollback`, `channel:edit` or `channel:rollout`.\n- No publishing when `compare` says a build is needed, and no editing the baseline file to make it pass.\n- No `EXPO_TOKEN` in files, logs or commands. It lives in CI secrets only.\n\n## Before you call it done\n\n1. `npx tsc --noEmit` is clean.\n2. `npx expo export --platform ios --platform android --output-dir dist` succeeds (the same bundle step an\n   update runs).\n3. `ota_check.mjs compare` result is stated in the summary, with the changed sources if a build is needed.\n"
    },
    {
      "path": "skills/eas-update-release/scripts/ota_check.mjs",
      "role": "reference",
      "description": "config, record and compare: fingerprint and update config checks.",
      "skill": "eas-update-release",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/eas-update-release/scripts/ota_check.mjs",
        "codex": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
        "cursor": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
        "gemini-cli": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
        "antigravity-cli": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
        "copilot": ".agents/skills/eas-update-release/scripts/ota_check.mjs"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/skills/eas-update-release/scripts/ota_check.mjs",
      "bytes": 6359,
      "lang": "markdown",
      "content": "#!/usr/bin/env node\n// Decide whether a change can ship as an EAS Update or needs a new store build.\n// Uses the project's own expo-updates CLI (fingerprint:generate) and app config; no Expo account needed.\n//\n//   node ota_check.mjs config                 check app config + eas.json are set up for safe updates\n//   node ota_check.mjs record  [file]         save the current native fingerprint (run when a build is cut)\n//   node ota_check.mjs compare [file]         exit 0 \"OTA-safe\" if native fingerprint unchanged, 1 if a build is needed\n//\n// file defaults to .eas/build-fingerprint.json. Platforms: ios and android (AL_PLATFORMS to override).\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { spawnSync } from \"node:child_process\";\nimport { createRequire } from \"node:module\";\n\nconst [cmd = \"\", fileArg] = process.argv.slice(2);\nconst file = fileArg ?? \".eas/build-fingerprint.json\";\nconst platforms = (process.env.AL_PLATFORMS ?? \"ios,android\").split(\",\").map((s) => s.trim()).filter(Boolean);\nconst npx = process.platform === \"win32\" ? \"npx.cmd\" : \"npx\";\n\nfunction runJson(args) {\n  const r = spawnSync(npx, [\"--no-install\", ...args], { encoding: \"utf8\", maxBuffer: 64 * 1024 * 1024 });\n  if (r.status !== 0) {\n    console.error(`failed: npx ${args.join(\" \")}\\n${(r.stderr || r.stdout || \"\").trim().slice(0, 2000)}`);\n    process.exit(2);\n  }\n  const out = r.stdout.slice(r.stdout.indexOf(\"{\"));\n  return JSON.parse(out);\n}\n\nconst keyOf = (s) => s.id ?? s.filePath ?? `${s.type}:${s.name ?? \"?\"}`;\n\nfunction fingerprint(platform) {\n  const fp = runJson([\"expo-updates\", \"fingerprint:generate\", \"--platform\", platform]);\n  return { hash: fp.hash, sources: Object.fromEntries(fp.sources.map((s) => [keyOf(s), s.hash])) };\n}\n\nasync function checkConfig() {\n  const problems = [];\n  const notes = [];\n  const cfg = runJson([\"expo\", \"config\", \"--json\", \"--type\", \"public\"]);\n  const rv = cfg.runtimeVersion;\n  for (const p of platforms) {\n    const prv = cfg[p]?.runtimeVersion ?? rv;\n    if (prv === undefined) problems.push(`${p}: no runtimeVersion; set { \"policy\": \"fingerprint\" }`);\n    else if (typeof prv === \"object\" && prv.policy !== \"fingerprint\") notes.push(`${p}: runtimeVersion policy \"${prv.policy}\" relies on people bumping versions when native code changes; \"fingerprint\" does it for you`);\n    else if (typeof prv === \"string\") notes.push(`${p}: runtimeVersion \"${prv}\" is manual; bump it with every native change or switch to { \"policy\": \"fingerprint\" }`);\n  }\n  const url = cfg.updates?.url;\n  if (!url || !/^https:\\/\\/u\\.expo\\.dev\\/[0-9a-f-]{36}$/.test(url)) problems.push(`updates.url is ${url ? `\"${url}\"` : \"missing\"}; expected https://u.expo.dev/<projectId> (run eas update:configure once)`);\n  const projectId = cfg.extra?.eas?.projectId;\n  if (url && projectId && !url.endsWith(projectId)) problems.push(`updates.url does not match extra.eas.projectId (${projectId})`);\n  if (cfg.updates?.enabled === false) problems.push(\"updates.enabled is false: builds will never download an update\");\n\n  if (!fs.existsSync(\"eas.json\")) problems.push(\"no eas.json\");\n  else {\n    // Validate and resolve \"extends\" with Expo's own eas.json parser (the @expo/eas-json dev dependency).\n    let easJson;\n    try { easJson = createRequire(path.resolve(\"package.json\"))(\"@expo/eas-json\"); }\n    catch { problems.push(\"@expo/eas-json is not installed: add it as a dev dependency pinned to your eas-cli version\"); }\n    if (easJson) {\n      const { EasJsonAccessor, EasJsonUtils } = easJson;\n      const acc = EasJsonAccessor.fromProjectPath(process.cwd());\n      const channels = new Map();\n      try {\n        for (const name of await EasJsonUtils.getBuildProfileNamesAsync(acc)) {\n          const prof = await EasJsonUtils.getBuildProfileAsync(acc, platforms[0], name);\n          if (prof.developmentClient) continue;\n          if (!prof.channel) { problems.push(`eas.json build.${name}: no channel, so its builds cannot receive updates`); continue; }\n          if (channels.has(prof.channel)) problems.push(`eas.json build.${name} and build.${channels.get(prof.channel)} share channel \"${prof.channel}\"`);\n          channels.set(prof.channel, name);\n        }\n        if (!channels.has(\"production\")) problems.push('eas.json: no build profile on channel \"production\"');\n      } catch (e) {\n        problems.push(e.message.replace(/\\n/g, \" \"));\n      }\n    }\n  }\n  for (const n of notes) console.log(`note: ${n}`);\n  for (const p of problems) console.log(`problem: ${p}`);\n  if (problems.length) { console.log(`config: ${problems.length} problem(s)`); process.exit(1); }\n  console.log(`config: ok (runtimeVersion ${JSON.stringify(rv)}, channels checked)`);\n}\n\nif (cmd === \"config\") await checkConfig();\nelse if (cmd === \"record\") {\n  const data = { recordedAt: new Date().toISOString(), platforms: Object.fromEntries(platforms.map((p) => [p, fingerprint(p)])) };\n  fs.mkdirSync(path.dirname(file), { recursive: true });\n  fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\\n`);\n  for (const p of platforms) console.log(`recorded ${p}: ${data.platforms[p].hash}`);\n} else if (cmd === \"compare\") {\n  if (!fs.existsSync(file)) { console.log(`no baseline at ${file}: run \"record\" when you cut the build`); process.exit(2); }\n  const base = JSON.parse(fs.readFileSync(file, \"utf8\")).platforms;\n  let changed = false;\n  for (const p of platforms) {\n    const now = fingerprint(p);\n    const was = base[p];\n    if (!was) { console.log(`${p}: no baseline recorded`); changed = true; continue; }\n    if (now.hash === was.hash) { console.log(`${p}: unchanged (${now.hash})`); continue; }\n    changed = true;\n    console.log(`${p}: native fingerprint changed ${was.hash} -> ${now.hash}`);\n    const keys = new Set([...Object.keys(was.sources), ...Object.keys(now.sources)]);\n    for (const k of [...keys].sort()) {\n      if (!(k in was.sources)) console.log(`  added: ${k}`);\n      else if (!(k in now.sources)) console.log(`  removed: ${k}`);\n      else if (was.sources[k] !== now.sources[k]) console.log(`  changed: ${k}`);\n    }\n  }\n  if (changed) { console.log(\"needs a new build: do not ship this as an EAS Update\"); process.exit(1); }\n  console.log(\"OTA-safe: native layer unchanged, an EAS Update can carry this change\");\n} else {\n  console.log(\"usage: ota_check.mjs config | record [file] | compare [file]\");\n  process.exit(2);\n}\n"
    },
    {
      "path": "skills/eas-update-release/references/app.json",
      "role": "reference",
      "description": "App config with the fingerprint policy and updates.url.",
      "skill": "eas-update-release",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/eas-update-release/references/app.json",
        "codex": ".agents/skills/eas-update-release/references/app.json",
        "cursor": ".agents/skills/eas-update-release/references/app.json",
        "gemini-cli": ".agents/skills/eas-update-release/references/app.json",
        "antigravity-cli": ".agents/skills/eas-update-release/references/app.json",
        "copilot": ".agents/skills/eas-update-release/references/app.json"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/skills/eas-update-release/references/app.json",
      "bytes": 571,
      "lang": "json",
      "content": "{\n  \"expo\": {\n    \"name\": \"Release Notes\",\n    \"slug\": \"release-notes\",\n    \"version\": \"1.4.0\",\n    \"owner\": \"acme\",\n    \"ios\": { \"bundleIdentifier\": \"com.acme.releasenotes\" },\n    \"android\": { \"package\": \"com.acme.releasenotes\" },\n    \"runtimeVersion\": { \"policy\": \"fingerprint\" },\n    \"updates\": {\n      \"url\": \"https://u.expo.dev/00000000-0000-4000-8000-000000000000\",\n      \"checkAutomatically\": \"ON_LOAD\",\n      \"fallbackToCacheTimeout\": 0\n    },\n    \"extra\": { \"eas\": { \"projectId\": \"00000000-0000-4000-8000-000000000000\" } },\n    \"plugins\": [\"expo-updates\"]\n  }\n}\n"
    },
    {
      "path": "skills/eas-update-release/references/eas.json",
      "role": "reference",
      "description": "Build profiles with one channel each.",
      "skill": "eas-update-release",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/eas-update-release/references/eas.json",
        "codex": ".agents/skills/eas-update-release/references/eas.json",
        "cursor": ".agents/skills/eas-update-release/references/eas.json",
        "gemini-cli": ".agents/skills/eas-update-release/references/eas.json",
        "antigravity-cli": ".agents/skills/eas-update-release/references/eas.json",
        "copilot": ".agents/skills/eas-update-release/references/eas.json"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/skills/eas-update-release/references/eas.json",
      "bytes": 402,
      "lang": "json",
      "content": "{\n  \"cli\": {\n    \"version\": \">= 25.1.1\",\n    \"appVersionSource\": \"remote\"\n  },\n  \"build\": {\n    \"development\": {\n      \"developmentClient\": true,\n      \"distribution\": \"internal\"\n    },\n    \"preview\": {\n      \"distribution\": \"internal\",\n      \"channel\": \"preview\"\n    },\n    \"production\": {\n      \"autoIncrement\": true,\n      \"channel\": \"production\"\n    }\n  },\n  \"submit\": {\n    \"production\": {}\n  }\n}\n"
    },
    {
      "path": "skills/eas-update-release/references/eas-update-preview.yml",
      "role": "reference",
      "description": "GitHub workflow: publish to preview only when OTA-safe.",
      "skill": "eas-update-release",
      "agent": null,
      "install_to": {
        "claude-code": ".claude/skills/eas-update-release/references/eas-update-preview.yml",
        "codex": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
        "cursor": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
        "gemini-cli": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
        "antigravity-cli": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
        "copilot": ".agents/skills/eas-update-release/references/eas-update-preview.yml"
      },
      "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
      "raw_latest_url": "https://loadout.jnw-it.com/raw/expo-eas-update/latest/skills/eas-update-release/references/eas-update-preview.yml",
      "bytes": 1376,
      "lang": "yaml",
      "content": "# .github/workflows/eas-update-preview.yml\n# On every push to main: typecheck, export the JS bundle, and publish an EAS Update to the\n# preview channel ONLY if the native fingerprint matches the last recorded build.\n# Production is promoted by a person (see the skill), never from this workflow.\nname: EAS Update (preview)\n\non:\n  push:\n    branches: [main]\n\npermissions:\n  contents: read\n\nconcurrency:\n  group: eas-update-preview\n  cancel-in-progress: false\n\njobs:\n  update:\n    runs-on: ubuntu-24.04\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v7\n      - uses: actions/setup-node@v7\n        with:\n          node-version: 22\n          cache: npm\n      - run: npm ci\n      - run: npx tsc --noEmit\n      - name: Native fingerprint unchanged since the last build\n        run: node .agents/skills/eas-update-release/scripts/ota_check.mjs compare\n      - name: Bundle builds\n        run: npx expo export --platform ios --platform android --output-dir dist\n      - name: Publish to preview\n        env:\n          EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}\n          # Passed through the environment, never interpolated into the script.\n          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}\n        run: >\n          npx --yes eas-cli@25.1.1 update\n          --channel preview --environment preview\n          --message \"$COMMIT_MESSAGE\" --non-interactive\n"
    }
  ],
  "requires": {
    "tools": [
      "node",
      "npm"
    ],
    "env": [],
    "mcp": []
  },
  "tested": {
    "status": "untested",
    "last_run": null,
    "fixture": null,
    "per_agent": {
      "claude-code": "untested",
      "codex": "untested",
      "cursor": "untested",
      "gemini-cli": "untested",
      "antigravity-cli": "untested",
      "copilot": "untested"
    }
  },
  "security": {
    "scanned": true,
    "scanner": "intake-precheck/1",
    "verdict": "clean",
    "network": true,
    "scripts": [
      "ota_check.mjs"
    ],
    "notes": "The check runs the project's own expo and expo-updates CLIs and makes no network calls. The example workflow publishes to the preview channel with an EXPO_TOKEN from GitHub secrets; agents never publish to production."
  },
  "featured": false,
  "deprecated": false,
  "last_checked": "2026-10-17",
  "published": "2026-10-17",
  "updated": "2026-10-17",
  "compatibility": {
    "claude-code": "native",
    "codex": "native",
    "cursor": "native",
    "gemini-cli": "native",
    "antigravity-cli": "native",
    "copilot": "native"
  },
  "counts": {
    "rules": 1,
    "skills": 1,
    "mcp": 0
  },
  "url": "https://loadout.jnw-it.com/loadouts/expo-eas-update/",
  "api_url": "https://loadout.jnw-it.com/api/loadouts/expo-eas-update.json",
  "verification": {
    "level": "verified-install",
    "status": "verified",
    "label": "Verified install",
    "verified": true,
    "checked_at": "2026-10-17T20:59:33Z",
    "commit": "08c3710727a4d6fc320a98ecf32bdc0a780eaa6c",
    "content_current": true,
    "agent_tests": "not-run",
    "counts": {
      "pass": 9,
      "fail": 0,
      "skip": 0,
      "not_applicable": 1
    },
    "meaning": "Level 1: automated checks on the files (parse, install paths, MCP config, safety scan, references, size, licence, starter build). No AI agent was run.",
    "checks": [
      {
        "id": "listing",
        "name": "Listing",
        "status": "pass",
        "summary": "Schema valid; 8 files declared, none missing or extra",
        "details": [
          "tested.status: untested (agent tests are separate from Level 1)"
        ]
      },
      {
        "id": "parse",
        "name": "Files parse",
        "status": "pass",
        "summary": "All 8 files parse; front matter valid",
        "details": []
      },
      {
        "id": "adapters",
        "name": "Adapters",
        "status": "pass",
        "summary": "CLAUDE.md and GEMINI.md import AGENTS.md and are up to date",
        "details": [
          "CLAUDE.md: matches the generator",
          "GEMINI.md: matches the generator"
        ]
      },
      {
        "id": "install-paths",
        "name": "Install paths",
        "status": "pass",
        "summary": "Correct paths for all 6 agents; layouts re-discovered",
        "details": [
          "Claude Code: 7 files, 1 skill in .claude/skills/",
          "Codex CLI: 6 files, 1 skill in .agents/skills/",
          "Cursor: 6 files, 1 skill in .agents/skills/",
          "Gemini CLI: 7 files, 1 skill in .agents/skills/",
          "Antigravity CLI: 6 files, 1 skill in .agents/skills/",
          "GitHub Copilot: 6 files, 1 skill in .agents/skills/"
        ]
      },
      {
        "id": "mcp",
        "name": "MCP config",
        "status": "n/a",
        "summary": "No MCP servers in this loadout",
        "details": []
      },
      {
        "id": "safety",
        "name": "Safety scan",
        "status": "pass",
        "summary": "Clean: no credentials, risky shell or hidden text",
        "details": [
          "Scanner intake-precheck/1: verdict clean; network no (listing says yes)"
        ]
      },
      {
        "id": "references",
        "name": "References",
        "status": "pass",
        "summary": "4 references between files resolve for every agent",
        "details": []
      },
      {
        "id": "size",
        "name": "Size limits",
        "status": "pass",
        "summary": "8 files, 14.7 KB total; largest skills/eas-update-release/scripts/ota_check.mjs 6.2 KB",
        "details": [
          "Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule"
        ]
      },
      {
        "id": "licence",
        "name": "Licence",
        "status": "pass",
        "summary": "MIT, by Agent Loadout; text in the repository LICENSE",
        "details": []
      },
      {
        "id": "starter-build",
        "name": "Starter build",
        "status": "pass",
        "summary": "The eas-update-release skill's app.json and eas.json pass ota_check config in an Expo SDK 58 app (expo-updates 58.0.17, @expo/eas-json 25.1.1); a JS edit and a version bump are OTA-safe, adding expo-haptics needs a new build, a profile with no channel and a misspelt key are caught, and the bundle ex",
        "details": [
          "Applied 13 loadout files at every agent's paths",
          "✓ npm ci (pinned Expo SDK 58): npm ci --no-audit --no-fund --ignore-scripts",
          "✓ place the skill's app.json and eas.json, check the update config: bash -c R=.agents/skills/eas-update-release/references; cp $R/app.json $R/eas.json . && node .agents/skills/eas-update-release/scripts/ota_check.mjs config",
          "✓ typecheck: npx tsc --noEmit",
          "✓ record the build fingerprint: node .agents/skills/eas-update-release/scripts/ota_check.mjs record",
          "✓ a JS change and a version bump are OTA-safe: bash -c sed -i 's/Release notes v1/Release notes v2/' App.tsx && sed -i 's/\"version\": \"1.4.0\"/\"version\": \"1.4.1\"/' app.json && grep -q 1.4.1 app.json && node .agents/skills/eas-update-release/scripts/ota_check.mjs compare",
          "✓ the bundle exports for both platforms: npx expo export --platform ios --platform android --output-dir dist",
          "✓ adding a native module needs a new build: bash -c npm install expo-haptics@58.0.7 --no-audit --no-fund --ignore-scripts >/dev/null && node .agents/skills/eas-update-release/scripts/ota_check.mjs compare; test $? -eq 1 && echo blocked",
          "✓ a profile with no channel and a misspelt key are caught: bash -c cp eas.json eas.good && node -e 'const f=require(\"fs\");const d=JSON.parse(f.readFileSync(\"eas.json\"));delete d.build.preview.channel;f.writeFileSync(\"eas.json\",JSON.stringify(d))' && node .agents/skills/eas-update-release/scripts/ota_check.mjs config; a=$?; node -e 'const f=require(\"fs\");const d=JSON.parse(f.readFileSync(\"eas.good\"));d.build.production.autoIncrment=true;f.writeFileSync(\"eas.json\",JSON.stringify(d))' && node .agen",
          "Covers: runtimeVersion fingerprint policy, updates.url matching the project ID, a channel per build profile",
          "Covers: eas.json validated with Expo's own @expo/eas-json parser, including extends",
          "Covers: Native fingerprint recorded at build time and compared before an update: JS and version bumps pass, a new native module fails",
          "Covers: expo export bundles for iOS and Android, the same step eas update runs"
        ]
      }
    ]
  },
  "install": {
    "claude-code": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "CLAUDE.md",
          "target": "CLAUDE.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/CLAUDE.md",
          "role": "adapter"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".claude/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".claude/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".claude/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".claude/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".claude/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/claude-code.zip"
    },
    "codex": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".agents/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".agents/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".agents/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/codex.zip"
    },
    "cursor": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".agents/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".agents/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".agents/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/cursor.zip"
    },
    "gemini-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "GEMINI.md",
          "target": "GEMINI.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/GEMINI.md",
          "role": "adapter"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".agents/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".agents/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".agents/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/gemini-cli.zip"
    },
    "antigravity-cli": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".agents/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".agents/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".agents/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/antigravity-cli.zip"
    },
    "copilot": {
      "files": [
        {
          "path": "AGENTS.md",
          "target": "AGENTS.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/AGENTS.md",
          "role": "instructions"
        },
        {
          "path": "skills/eas-update-release/SKILL.md",
          "target": ".agents/skills/eas-update-release/SKILL.md",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/SKILL.md",
          "role": "skill"
        },
        {
          "path": "skills/eas-update-release/scripts/ota_check.mjs",
          "target": ".agents/skills/eas-update-release/scripts/ota_check.mjs",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/scripts/ota_check.mjs",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/app.json",
          "target": ".agents/skills/eas-update-release/references/app.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/app.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas.json",
          "target": ".agents/skills/eas-update-release/references/eas.json",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas.json",
          "role": "reference"
        },
        {
          "path": "skills/eas-update-release/references/eas-update-preview.yml",
          "target": ".agents/skills/eas-update-release/references/eas-update-preview.yml",
          "raw_url": "https://loadout.jnw-it.com/raw/expo-eas-update/1.0.0/skills/eas-update-release/references/eas-update-preview.yml",
          "role": "reference"
        }
      ],
      "manual_steps": [],
      "zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/copilot.zip"
    }
  },
  "source_zip_url": "https://loadout.jnw-it.com/download/expo-eas-update/source.zip"
}