# Automatic PR Previews with GitHub Actions
URL: /docs/ios/pr-previews
LLM index: /llms.txt
Description: On every pull request, build the iOS app on Limrun, upload it to Asset Storage, and post a live preview link as a PR comment.

# Automatic PR Previews with GitHub Actions

An iOS PR preview works like a web deploy preview: every pull request gets its own build and its own link, and reviewers open the build in a browser. Limrun does the build, the asset upload, and the preview URL. GitHub Actions handles the workflow trigger and the PR comment.

## What you get on the PR

After the workflow runs, the PR gets a comment with a preview link:

```markdown
📱 **iOS preview ready**

Preview: https://console.limrun.com/preview?asset=my-app-pr-42.zip&platform=ios
```

Open the link in a browser. The Limrun console resolves the asset name to a freshly provisioned simulator with that build pre-installed.

## Use the packaged Action (recommended)

[`limrun-inc/ios-preview-action`](https://github.com/limrun-inc/ios-preview-action) wraps the create, build, upload, and PR-comment steps into one. It cleans up the Xcode sandbox in a `post:` step automatically, so you don't need a separate cleanup job.

Set `LIM_API_KEY` in **Settings → Secrets and variables → Actions** on the GitHub repo, then drop this in:

```yaml title=".github/workflows/ios-preview.yml"
name: iOS Preview

on:
  pull_request:
    types: [opened, synchronize, reopened, closed]

permissions:
  contents: read
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: limrun-inc/ios-preview-action@main
        with:
          project-path: .
          api-key: ${{ secrets.LIM_API_KEY }}
```

Inputs: `project-path`, `project`, `workspace`, `scheme`, `sdk`, `api-key`, `github-token`. Outputs: `preview-url` and `asset-name`.

The Action posts a markdown-table comment with the platform, the short SHA, and the preview link. `closed` in the `on.pull_request.types` array triggers the post-step cleanup when the PR merges or closes.

## Custom workflow

Write the workflow by hand when you need finer control over the comment format, an asset-naming scheme different from the Action's default, or a runner outside GitHub Actions. The CLI is plain Node, so the same `lim xcode build --upload` pattern works on GitLab CI, CircleCI, Buildkite, Jenkins, and anywhere else `npm i -g @limrun/cli` runs.

End-to-end: build on every PR open or sync, upload under a PR-scoped asset name, post the preview link as a comment. Replace `MyApp` with your scheme name.

```yaml title=".github/workflows/ios-preview.yml"
name: iOS Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  pull-requests: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Limrun CLI
        run: npm install --global @limrun/cli

      - name: Build on Limrun
        env:
          LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
          PR: ${{ github.event.number }}
          REPO: ${{ github.event.repository.name }}
        run: |
          ASSET_NAME="${REPO}-pr-${PR}.zip"
          echo "ASSET_NAME=${ASSET_NAME}" >> $GITHUB_ENV

          lim xcode create --reuse-if-exists \
            --label pr=${PR} \
            --label repo=${REPO}

          lim xcode build . \
            --scheme MyApp \
            --upload "${ASSET_NAME}"

      - name: Comment with preview link
        uses: actions/github-script@v7
        with:
          script: |
            const assetName = process.env.ASSET_NAME;
            const url = `https://console.limrun.com/preview?asset=${encodeURIComponent(assetName)}&platform=ios`;
            const body = [
              '📱 **iOS preview ready**',
              '',
              `Preview: ${url}`,
            ].join('\n');
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body,
            });
```

### What each step does

1. **Checkout.** The source tree is what gets synced to the Xcode sandbox.
2. **Install Limrun CLI.** `npm i -g @limrun/cli` runs anywhere Node does. The build itself runs on Limrun's Mac fleet, so the job stays on a cheap Linux runner.
3. **Build on Limrun.** `lim xcode create --reuse-if-exists` provisions (or reuses) a sandbox tagged with the PR number and repo name, so subsequent pushes to the same PR reuse the same sandbox instead of spinning up a fresh one. `lim xcode build . --upload <name>` then syncs the source, runs `xcodebuild`, and uploads the artifact to [Asset Storage](/docs/platform/asset-storage). Naming the asset after the PR means the next push overwrites it, so the preview link always points at the latest commit.
4. **Comment with preview link.** Build `https://console.limrun.com/preview?asset=<name>&platform=ios` and post it as a PR comment. Opening the link in the Limrun console provisions a simulator and pre-installs the asset uploaded in step 3.

## Clean up when a PR closes

The packaged Action handles this in its `post:` step. For the custom workflow, add a cleanup job that deletes everything tagged with the closed PR:

```yaml
  cleanup:
    if: github.event.action == 'closed'
    runs-on: ubuntu-latest
    steps:
      - run: npm install --global @limrun/cli
      - name: Delete PR instances and assets
        env:
          LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
          PR: ${{ github.event.number }}
          REPO: ${{ github.event.repository.name }}
        run: |
          # Delete Xcode sandboxes for this PR
          lim xcode list --label-selector "pr=${PR},repo=${REPO}" --json \
            | jq -r '.[].metadata.id' \
            | xargs -r -n1 lim xcode delete

          # Delete the preview asset
          ASSET_NAME="${REPO}-pr-${PR}.zip"
          ASSET_ID=$(lim asset list --name "${ASSET_NAME}" --json | jq -r '.[0].id // empty')
          if [ -n "$ASSET_ID" ]; then
            lim asset delete "$ASSET_ID"
          fi
```

Trigger it from the same workflow file by adding `closed` to the `on.pull_request.types` array.

## Next steps

<Cards>
  <Card title="Build with remote Xcode" icon="hammer" href="/docs/ios/build-with-xcode">
    All the `lim xcode build` flags. Real-device signed IPAs live there.
  </Card>
  <Card title="Asset Storage" icon="package" href="/docs/platform/asset-storage">
    How the uploaded build artifact is stored, accessed, and cleaned up.
  </Card>
</Cards>