← LimrunCONTENT HISTORY

Update to Limrun

Snapshot Sep 30, 2026 · 23:08 UTC · version 1.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "limrun-xcode",
  "description": "Build an iOS / Apple app on remote Xcode with `lim xcode build` instead of local xcodebuild, or run its XCTest suites with `lim xcode test`, from any environment (Linux, Windows, macOS, VM, container). Use for non-Bazel projects (an `.xcodeproj` / `.xcworkspace`, an XcodeGen `project.yml` with a gitignored project, React Native / Expo native build) when the user wants to build, compile, test, inspect build logs, reload, produce a preview build, or ship a signed device IPA. To run, tap, screenshot, or otherwise interact with the result on a simulator, use limrun-ios-simulator. For Bazel workspaces, use limrun-xcode-bazel.",
  "included_files": [],
  "skill_md_contents": "---\nname: limrun-xcode\ndescription: \"Build an iOS / Apple app on remote Xcode with `lim xcode build` instead of local xcodebuild, or run its XCTest suites with `lim xcode test`, from any environment (Linux, Windows, macOS, VM, container). Use for non-Bazel projects (an `.xcodeproj` / `.xcworkspace`, an XcodeGen `project.yml` with a gitignored project, React Native / Expo native build) when the user wants to build, compile, test, inspect build logs, reload, produce a preview build, or ship a signed device IPA. To run, tap, screenshot, or otherwise interact with the result on a simulator, use limrun-ios-simulator. For Bazel workspaces, use limrun-xcode-bazel.\"\nuser-invocable: true\neffort: high\n---\n\n# Remote Xcode build\n\nBuild Apple projects on Limrun's remote Xcode, from any environment (Linux,\nWindows, macOS, VM, container). `lim xcode build` syncs your sources to a remote\nXcode instance, builds there, and (when a simulator is attached) installs and\nrelaunches the app. Never fall back to local Xcode, local simulators, or local\nbuild tools. Your job doesn't end at a green build: get the app running, verify\nit works, and iterate until the user is satisfied.\n\nFor driving the app once it's running (tap, type, element tree, screenshot,\nrecord), use the **`limrun-ios-simulator`** skill. For Bazel workspaces, use\n**`limrun-xcode-bazel`** instead of this skill.\n\n## Auth and CLI\n\nInstall if needed: `npm install --global lim`. Auth is `lim login` or\n`LIM_API_KEY` (it may be set outside the project, so don't ask for it just\nbecause it's missing from `.env` or the shell). The CLI is the source of truth:\nthe commands in this skill are verified, but if a flag errors or you need one\nnot shown here, check `--help` instead of guessing:\n\n```bash\nlim xcode --help\nlim xcode build --help\n```\n\n## Build\n\nInstead of `xcodebuild`, build with:\n\n```bash\nlim xcode build .\n```\n\nThis creates or reuses the remembered Xcode target, syncs the current directory,\nand streams the build logs through stdout and stderr.\n\nUse `--scheme` and `--workspace` if the project has multiple schemes or uses a\nworkspace file:\n\n```bash\nlim xcode build . --scheme MyApp --workspace MyApp.xcworkspace\n```\n\nUse `--configuration Debug` or `--configuration Release` for a specific Xcode\nconfiguration. If omitted, Limrun uses limbuild's project-type default: `Debug`\nfor native Xcode builds, `Release` for React Native / Expo builds.\n\n```bash\nlim xcode build . --configuration Debug\n```\n\n### Detached builds and logs\n\nUse `--detach` to return once the build is accepted; a webhook is optional.\n`logs` reads the latest build without an exec ID, including persisted logs after\ninstance deletion; add `--follow` to wait for completion.\n\n```bash\nlim xcode build . --detach\nlim xcode logs\nlim xcode logs --follow\n```\n\n### Pick the Xcode version\n\nA sandbox builds with its node's default Xcode. To build with another installed\nmajor (Xcode 27 beta is available beside the default), set a preference once\nfor the workspace; every later build, test, RBE session and new sandbox follows\nit, and the flag overrides it for one command:\n\n```bash\nlim xcode version list      # versions the sandbox can build with; * marks the one in use\nlim xcode use xcode@27      # prefer 27 for this workspace; switches the remembered sandbox now\nlim xcode build .           # builds with 27\nlim xcode version           # \"27.0 (27A5252f)\" shows the sandbox's current Xcode\nlim xcode build . --xcode-version 26   # one-off override, not remembered\nlim xcode version unset     # forget the preference; the sandbox goes back to the node default\n```\n\nCombine Xcode and mise selections with `lim xcode use xcode@27 node@24`.\n\nFor scripting, `lim xcode version list --quiet` prints one selectable major per\nline and `--json` returns `{ installed, bound, preferred }` (`installed[].betaSeed`\ncarries the beta seed). The table marks the Xcode in use with `*`.\n`lim xcode version set` does not record a major the node lacks (the error lists\nthe available ones) but keeps it when the sandbox is merely busy.\n\nWhen the sandbox is on another major than the workspace prefers, the next\nbuild says so and switches it first. Switching invalidates the build cache made\nwith the other version (the next build starts cold) and is refused while a build,\nsync or `lim xcode rbe` stack is running. A major the node does not have fails with the available list;\nonly majors are selectable. App Store uploads from a beta Xcode are rejected by\nApple, so keep `--upload-to-appstore` on the default.\n\n`--dev-server-url` is only supported with `--configuration Debug` for React\nNative / Expo builds. It's a post-install launch URL: limbuild validates it is a\nparseable absolute URL, then opens it unchanged after installing on the attached\nsimulator. Framework-specific skills construct the correct URL.\n\n```bash\nlim xcode build . --configuration Debug --dev-server-url '<absolute-url>'\n```\n\nIf the app launches without using the expected URL, open it explicitly to\nseparate build/install issues from URL routing:\n\n```bash\nlim ios open-url --id <ios-instance-id> '<absolute-url>'\n```\n\n## Developer tool versions\n\nAfter syncing, `lim xcode use` selects tools in the sandbox and installs missing versions.\nRun `lim xcode tools install` for synced project tool selections ([details](https://docs.limrun.com/docs/ios/build-with-xcode)). Use major versions, or major.minor for Ruby, Flutter, and pre-1.0 tools such as Mint.\n\n```bash\nlim xcode tools\n# Node includes npm/npx, Ruby includes gem, Flutter includes Dart, CocoaPods includes cocoapods-patch.\nlim xcode use node@24 pnpm@10 yarn@4 bun@1 ruby@3.3 bundler@4 cocoapods@1 \\\n  cmake@3 java@jetbrains-21 corretto@21 flutter@3.44 mint@0.18 \\\n  xcodegen@2 xcbeautify@3 zsign@1\nlim xcode tools install\nlim xcode use --cwd apps/mobile node@24\nlim xcode tools install --cwd apps/mobile\nlim xcode run -- mise use --pin node@24.5.0\n```\n\n## Generated Xcode projects (XcodeGen)\n\nIf the repo has a `project.yml` and the `.xcodeproj` is gitignored, do not run\nxcodegen locally and do not treat the missing project as an error. The remote\nsandbox generates the project from `project.yml` before building:\n\n```bash\nlim xcode build .\n```\n\nThe spec is found at the repo root or one directory down (like `ios/`), no\nflags needed. The project regenerates on every build, so `project.yml` edits\ntake effect by just rebuilding. A committed or force-synced `.xcodeproj`\nalways wins: the sandbox only generates when the sync didn't supply one.\n\nIf the repo's codegen produces gitignored inputs the build needs (a generated\nlocal Swift package, config-derived sources), run that step locally first and\nforce-sync its output with `--include`:\n\n```bash\nmake generate   # or whatever the repo's codegen step is\nlim xcode build . --include '^ios/GeneratedKit/'\n```\n\n`--include` takes a regular expression like `--ignore`, not gitignore syntax.\nTo reach files under a directory that is ignored as a whole, the pattern must\nalso match the directory path itself, as above.\n\n## Run on a simulator\n\n`lim xcode build` is build-and-install. Don't attach a simulator until the user\nneeds to see or interact with the app. Check / attach:\n\n```bash\nlim xcode get             # is a simulator already attached?\nlim ios create --attach   # attach one (installs the last build immediately)\n```\n\nAdd `--no-open` when you have no browser to show the user; it skips opening\nthe stream URL locally and still prints it for sharing.\n\nIf the attach output includes a signed stream URL, share it with the user as a\nMarkdown link, such as `[Live simulator](<signed-stream-url>)`.\n\nWhen a simulator is attached, every successful `lim xcode build` automatically\nreinstalls and relaunches the app, no separate install step. To tap, type, read\nthe element tree, screenshot, or record, switch to **`limrun-ios-simulator`**.\n\n## Run tests (XCTest)\n\n`lim xcode test` builds the scheme's test targets on the sandbox, runs them on\nan attached simulator (unit and UI targets alike), and streams one line per\ntest case. The exec exits non-zero when any test fails, so it works as a CI\ngate.\n\n```bash\nlim xcode test .\nlim xcode test ./MyProject --scheme MyApp\n```\n\nIt auto-acquires a simulator-backed target like `lim xcode build --ios` and\nreuses the instances on repeat runs, so iterating is fast. The scheme must\nhave a test action configured (shared schemes from Xcode have one when the\nproject has test targets). `--xcode-version 27` builds the tests with that\nXcode; the simulator keeps the fleet default runtime, so the run warns and\nproceeds (runtime-dependent failures are possible).\n\nSelect a subset with xcodebuild's identifier format\n`Target[/Class[/method]]`; repeat the flag for multiple entries. The two flags\nare mutually exclusive:\n\n```bash\nlim xcode test . --only-testing MyAppTests/LoginTests/testValidLogin\nlim xcode test . --skip-testing MyAppUITests\n```\n\nA bare target name selects or skips that whole target. An `--only-testing`\nentry naming a target the build did not produce fails the run instead of\nsilently running everything.\n\nFor machine consumption, `--json` streams the raw per-case events as NDJSON\nand ends with a `{\"exitCode\": N}` record:\n\n```bash\nlim xcode test . --json > results.ndjson\n```\n\n`--build-only` compiles the test targets without acquiring a simulator; the\nproducts stay on the sandbox for a later run.\n\nIf UI tests fail at the very first interaction on an instance that has run\nmany suites back to back, prefer fresh instances with\n`--inactivity-timeout 30m` on the next run.\n\n## Signed device builds (IPA)\n\nPrefer Apple cloud signing when the user has an App Store Connect team API key.\nApple creates or reuses a cloud-managed certificate and provisioning profile,\nso the user does not need to supply a p12 or `.mobileprovision`:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --signing-method release-testing --team-id \"$APPLE_TEAM_ID\" \\\n  --asc-key-id \"$ASC_KEY_ID\" --asc-issuer-id \"$ASC_ISSUER_ID\" \\\n  --asc-key AuthKey.p8 \\\n  --upload myapp.ipa\n```\n\nSigning methods:\n\n- `debugging`: development-signed IPA for registered development devices.\n- `release-testing`: distribution-signed IPA for registered test devices.\n- `app-store-connect`: distribution-signed IPA for App Store Connect.\n\nCloud signing requires a device SDK (`--sdk iphoneos` or `--sdk watchos`), a\nteam API key with an issuer ID, and `--team-id` matching that key's Apple\nDeveloper team. For distribution methods,\nthe API key must be an Admin key or have **Access to cloud-managed distribution\ncertificates** enabled. A `Cloud signing permission error` means that permission\nis missing. `No Account for Team` means the team ID and API key do not match.\n`Failed Registering Bundle Identifier` means the bundle ID belongs to another\nteam and cannot be registered automatically.\n\nCloud signing takes entitlements only from `--entitlements`, never from the\nproject's `.entitlements` file: the archive is unsigned and the export\npreserves entitlements only from an existing code signature. Any app using\ncapabilities (HealthKit, CloudKit, app groups, push) MUST pass the flag or\nthe capability is silently stripped from the IPA. A bare path targets the\napp; `<bundleId>=<path>` targets an embedded bundle (widget, watch app);\nrepeat per bundle:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --signing-method release-testing --team-id VMBY3VYW4U \\\n  --asc-key-id 2X9R4HXF34 --asc-issuer-id \"$ASC_ISSUER_ID\" \\\n  --asc-key AuthKey.p8 \\\n  --entitlements ./MyApp/MyApp.entitlements \\\n  --entitlements com.example.myapp.widgets=./Widgets/Widgets.entitlements \\\n  --upload myapp.ipa\n```\n\nThe plist values must be fully expanded (no `$(AppIdentifierPrefix)`; write\nthe concrete prefix), must omit export-managed keys (`application-identifier`,\n`com.apple.developer.team-identifier`, `get-task-allow`,\n`beta-reports-active`), and every capability must be enabled on the App ID in\nthe developer portal or the export fails naming it.\n\nManual signing remains available when the user already has a p12 and profiles:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --certificate-p12 dist.p12 --certificate-password \"$P12_PASSWORD\" \\\n  --provisioning-profile app.mobileprovision \\\n  --upload myapp.ipa\n```\n\nThe upload output includes a download URL for the signed IPA. A SUCCEEDED build\nmeans the signature already passed Apple's verifier on the server, so don't\nre-verify the IPA yourself unless the user asks. Invalid signing fails the\nbuild loudly instead of producing a broken artifact.\n\nIf the app embeds extensions (WidgetKit widgets, share sheets, intents) or a\nwatch app, App Store signing needs one provisioning profile per bundle id, all\nissued for the same distribution certificate. Repeat `--provisioning-profile`\nonce per bundle; each profile is matched to its bundle by the\napplication-identifier inside it, so order doesn't matter:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --certificate-p12 dist.p12 --certificate-password \"$P12_PASSWORD\" \\\n  --provisioning-profile app.mobileprovision \\\n  --provisioning-profile widgets.mobileprovision \\\n  --upload myapp.ipa\n```\n\nWith multiple profiles, every profile must carry an explicit (non-wildcard)\nbundle id. A `signing preflight failed: no provisioning profile covers ...`\nerror names the embedded bundle that lacks a profile; ask the user for a\nprofile with exactly that bundle id.\n\nUse a p12 that includes its full CA chain, not just the leaf certificate. If\nneeded, re-export it with the chain:\n\n```bash\nopenssl pkcs12 -export -inkey dist.key -in dist.pem -certfile wwdr.pem -out dist-chain.p12\n```\n\nFailure strings to recognize in the build output:\n\n- `Unknown issuer hash`: the p12 lacks its CA chain; re-export it with the\n  chain as above.\n- `code signature verification failed`: the platform's post-sign check rejected\n  the artifact. Not a problem in the user's code; retry, and report it if it\n  persists.\n- p12 password errors: `--certificate-password` doesn't match the file; ask the\n  user for the right password.\n\n## Upload to App Store Connect\n\nTo upload the signed IPA to App Store Connect for TestFlight or App Store\ndistribution, pass `--upload-to-appstore` with the App Store Connect API key\nflags on a signed device build:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --certificate-p12 dist.p12 --certificate-password \"$P12_PASSWORD\" \\\n  --provisioning-profile app.mobileprovision \\\n  --upload-to-appstore --asc-key-id \"$ASC_KEY_ID\" --asc-issuer-id \"$ASC_ISSUER_ID\" \\\n  --asc-key AuthKey.p8\n```\n\nCloud signing can sign and upload with the same API key:\n\n```bash\nlim xcode build . --sdk iphoneos --configuration Release \\\n  --signing-method app-store-connect --team-id \"$APPLE_TEAM_ID\" \\\n  --asc-key-id \"$ASC_KEY_ID\" --asc-issuer-id \"$ASC_ISSUER_ID\" \\\n  --asc-key AuthKey.p8 \\\n  --upload-to-appstore --auto-build-number\n```\n\n`--upload-to-appstore` requires either cloud signing or the manual signing\nflags, plus `--asc-key-id` and `--asc-key`. Combine it with `--upload\n<asset-name>` when the user also wants the IPA in Asset Storage.\n\nDevice IPAs carry the app's symbols (`Symbols/` next to `Payload/`), so App\nStore Connect symbolicates crash reports without a separate dSYM upload. This\nneeds the build to produce dSYMs: `--configuration Release` does by default;\nDebug does not, and the IPA then simply ships without symbols.\n\nCollect from the user (all three live in App Store Connect under Users and\nAccess, Integrations tab, App Store Connect API):\n\n- `--asc-key-id`: the Key ID next to their API key. If they don't have one,\n  point them at Team Keys with the **Developer** role: the least-privileged\n  role that can upload builds. Creating team keys needs an Admin account.\n- `--asc-issuer-id`: the Issuer ID at the TOP of the Integrations page (a\n  team value, not per-key). Cloud signing requires a team key and this flag.\n  For manual signing plus upload, omit it for individual API keys.\n- `--asc-key`: path to the downloaded `.p8` file. Apple keeps no copy and\n  the download link disappears after leaving the page; if the user lost it,\n  they must generate a new key. Never commit the `.p8` or paste its content\n  into files; pass a filesystem path.\n\nBy default the build returns as soon as the upload commits and leaves Apple's\nprocessing verdict to App Store Connect (processing routinely takes many\nminutes). Pass `--asc-wait-timeout <seconds>` (max 1800) to watch for the\nverdict before returning. Read the outcome from the final lines:\n\n- `App Store Connect: upload accepted.`: done; the build appears in\n  TestFlight once Apple finishes.\n- `App Store Connect: uploaded, still processing on Apple's side (upload\n  <id>).`: the exit code is 0 and the upload succeeded; Apple is still\n  processing. Do NOT retry the build.\n- `App Store Connect upload failed; the build and signing succeeded.`: exit\n  code 1 with Apple's error text earlier in the log. Only the delivery\n  failed.\n\nFailure strings to recognize:\n\n- Apple text about the bundle version being already used: bump\n  `CFBundleVersion` (Expo: `expo.ios.buildNumber` in app.json) and rebuild.\n- `HTTP 401`: key ID / issuer ID / .p8 mismatch, or a revoked key.\n- `HTTP 403`: the key's role cannot upload builds; it needs the Developer role\n  or higher.\n- `no App Store Connect app with bundle id`: the app record doesn't exist;\n  the user must create it in App Store Connect manually (the API cannot).\n- Build later stuck at \"Missing Compliance\" in TestFlight: the app doesn't\n  answer the export-compliance question at build time. Set\n  `ITSAppUsesNonExemptEncryption` to `NO` in Info.plist (Expo:\n  `expo.ios.config.usesNonExemptEncryption: false` in app.json) and rebuild.\n\nFor hands-off delivery to testers, the app's internal TestFlight group must\nhave automatic distribution enabled (create-only setting) and the compliance\nkey above must be set; then no post-upload steps exist at all.\n\n## Preview builds\n\nOnly create a reusable preview asset when the user asks for a preview build or\nwhen you're opening a PR. Build and upload:\n\n```bash\nASSET_NAME=\"<bundle id / pr number / or any session identifier>.zip\"\nlim xcode build . --upload ${ASSET_NAME}\n# Debug preview build:\nlim xcode build . --configuration Debug --upload ${ASSET_NAME}\n```\n\nBuild uploads default to a 14-day TTL: each build pushes the asset's expiry\nto 14 days from that upload. Pass `--upload-ttl` with a Go duration (e.g.\n`720h`; `1d` is invalid) to change it.\n\nThen construct the preview link and include it in your last message (and in the\nPR, if you're opening one):\n\n```\nhttps://console.limrun.com/preview?asset=${ASSET_NAME}&platform=ios\n```\n\n## Gotchas\n\n- **Build errors are your job to fix.** If a build fails, read the error output,\n  fix the code, and rebuild. Don't ask the user to fix build errors.\n- **Instance ID for `lim ios` commands.** They resolve the current instance\n  from the git worktree of your cwd and can fail with `No instance ID provided\n  and no recent ios instance found`. Get the ID from `lim xcode get` and pass\n  `--id <ios-instance-id>`; full recipe in limrun-ios-simulator's \"Targeting\n  the right instance\" section.\n- **Bundle ID discovery.** If you don't know the bundle ID, check the Xcode\n  project files or run `lim ios list-apps` after a successful build.\n- **Auth errors** on an authenticated command mean the session expired or\n  `LIM_API_KEY` is wrong; ask the user to run `lim login` or provide a key.\n- **Build settings override Limrun's defaults.** `--build-setting KEY=VALUE`\n  accepts any environment-style key and replaces the managed value with the\n  same key. Device builds already use standard architectures (an embedded\n  watch app keeps `arm64_32`) and disable coverage instrumentation, so App\n  Store uploads need no extra settings.\n- **Artifact not found after a successful build.** The server resolves the\n  built .app on its own, including when the scheme name differs from the\n  product name (scheme \"MyApp Dev\" building MyApp-dev.app). If an upload\n  still fails with `built artifact not found`, pass the full bundle filename\n  explicitly with `--artifact-name MyApp-dev.app` (including the .app\n  extension); the server then takes that name from the build products\n  verbatim.\n- **Keep synced files small.** A single ~2MB+ file can fail the client-side\n  sync with ENOMEM before the build starts; compress large assets.\n- **Symlinks sync when relative and in-root.** A symlink whose target is an\n  absolute path is skipped with a warning; recreate it with a relative target\n  if the build needs it. A relative link escaping the synced folder fails the\n  sync; `--ignore` it or sync from the repo root that contains the target.\n- **Signing failures are loud and specific.** `Unknown issuer hash` means the\n  p12 lacks its CA chain, so re-export it with the chain; `code signature\n  verification failed` means the platform's post-sign check rejected the\n  artifact, which is not a code problem, so retry or report it.\n"
}

SHA-256: e86d71b2ec838d9fc5019752b6b2b1861348988b586f47dc6e3afc64a92d217e