Llim.run

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:

📱 **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.

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:

.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.

.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. 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:

  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

hammer

Build with remote Xcode

All the lim xcode build flags. Real-device signed IPAs live there.

package

Asset Storage

How the uploaded build artifact is stored, accessed, and cleaned up.