GitHub Actions recipes
Every build on this page runs on a standard ubuntu-latest runner. The Macs, the Xcode install, the Android SDK, and the JDK live on Limrun's side, so the runner only needs Node to install the lim CLI and a LIM_API_KEY secret. No runs-on: macos-latest, no Apple-licensed CI runner.
Add the key under Settings, Secrets and variables, Actions in your repository as LIM_API_KEY. Signing secrets that a recipe needs are listed with it.
Build a signed IPA on every pull request
This job signs a device build with a p12 certificate and a provisioning profile, then uploads the IPA to Asset Storage. Store the certificate and profile as base64 secrets (P12_BASE64, PROFILE_BASE64) along with P12_PASSWORD:
name: iOS build
on: { pull_request: { types: [opened, synchronize] } }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install --global lim
- name: Build
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
P12_BASE64: ${{ secrets.P12_BASE64 }}
PROFILE_BASE64: ${{ secrets.PROFILE_BASE64 }}
run: |
echo "$P12_BASE64" | base64 -d > /tmp/dist.p12
echo "$PROFILE_BASE64" | base64 -d > /tmp/MyApp.mobileprovision
lim xcode create --reuse-if-exists \
--label repo=${{ github.event.repository.name }} \
--label pr=${{ github.event.number }}
lim xcode build . \
--scheme MyApp \
--sdk iphoneos \
--certificate-p12 /tmp/dist.p12 \
--certificate-password "$P12_PASSWORD" \
--provisioning-profile /tmp/MyApp.mobileprovision \
--upload my-app-pr-${{ github.event.number }}.ipa
- name: Delete the sandbox
if: always()
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: |
lim xcode list --all \
--label-selector "repo=${{ github.event.repository.name }},pr=${{ github.event.number }}" --json \
| jq -r '.[].metadata.id' \
| xargs -r -n1 lim xcode deleteWhat the workflow does:
- Checks out the PR's code on an Ubuntu runner.
- Installs the
limCLI withnpm. - Decodes the signing certificate and provisioning profile from the base64 secrets onto the runner's filesystem. The secrets reach the script through
env:, never interpolated into the script body. - Creates an Xcode sandbox labelled with the repository and PR number, so the cleanup step can find it. If you drop the cleanup step, the same labels and
--reuse-if-existslet later pushes to the PR reuse the warm sandbox. - Runs a signed device build and uploads the IPA under a name tied to the PR number. The CLI prints the artifact download URL to the job log.
- Deletes the sandbox by label, even when the build fails. Drop this step if you would rather keep a warm sandbox between pushes and let its inactivity timeout clean it up.
To sign without a p12 at all, or to upload straight to TestFlight, swap the signing flags for cloud signing and --upload-to-appstore; see Sign and distribute.
Build a Bazel workspace
For Bazel workspaces, start the remote build execution endpoint, build with the printed command, upload the result, and stop the stack:
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install --global lim
- name: Build on Limrun RBE
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: |
lim xcode rbe
bazelisk --digest_function=sha256 build --config=limrun //App
lim xcode rbe upload my-app-pr-${{ github.event.number }}
- name: Stop the stack and delete the sandbox
if: always()
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: |
lim xcode rbe --stop
lim xcode deletelim xcode rbe upload publishes the latest successful build before the cleanup step runs. lim xcode rbe --stop stops only the tunnel and the remote stack, so the step also deletes the Xcode sandbox, even when the build fails. Prefer it over --auto-upload in CI: an automatic upload runs after the build returns, and stopping the stack right away can cancel it. Keep --digest_function=sha256 in front of build; Build with Bazel explains why and covers the generated --config=limrun.
Build a signed AAB on every push
This job builds a release AAB signed with your organization's escrowed upload key, so no Android SDK, JDK, or keystore exists anywhere in the pipeline:
name: Android release build
on: { push: { branches: [main] } }
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install --global lim
- name: Build signed AAB
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: lim gradle build . --sign --upload myapp-${{ github.sha }}.aab
- name: Delete the sandbox
if: always()
env:
LIM_API_KEY: ${{ secrets.LIM_API_KEY }}
run: lim gradle deleteWhat the workflow does:
- Checks out the code on an Ubuntu runner.
- Installs the
limCLI withnpm. - Runs a signed release build on a Limrun Gradle sandbox and uploads the AAB under a name tied to the commit. The CLI prints the asset name and a signed download URL, which expires after 15 minutes.
- Deletes the Gradle sandbox the build used, even when the build fails.
To download the AAB later, ask Asset Storage for a fresh signed URL:
lim asset list --name myapp-<commit-sha>.aab --download-urlThe only secret in the pipeline is LIM_API_KEY. The upload keystore never exists on the runner, so there is nothing to rotate if a CI provider is compromised. Sign and publish covers how --sign escrows the key and how to bring your own.
Run tests and previews
Test and preview workflows live next to the tools they run:
- PR previews: build every pull request and comment with a preview link, including cleanup when the PR closes.
- XCTest:
lim xcode test .exits non-zero when any test fails, so it works as a CI step with no output parsing. - Maestro and Appium: complete Linux runner workflows for each framework.
Delete the instances a job creates in an if: always() step, by label as the IPA recipe does or as the last instance of its type as the other recipes do. Inactivity timeouts clean up after jobs that crash before that step runs.
Next steps
Build logs and webhooks
Detach from long builds and receive the result on your own endpoint.
PR previews
A live preview link on every pull request.
Sign and distribute
Cloud signing, extensions, and TestFlight uploads.
Sign and publish
Escrowed upload keys and Google Play publishing.
Was this guide helpful?