greenroom
Quickstart

iOS quickstart

Put Greenroom on an iOS app that builds for the Simulator.

The iOS setup is the same four files as web, with one structural difference: your workflow first builds a simulator .app in a job with no secrets, and Greenroom runs against that artifact in a second job. Native Swift, SwiftUI, and React Native / Expo apps all work; the requirement is a simulator build with no dev-server dependency.

1. Install the GitHub App

Same as web: install the Greenroom GitHub App on the repository. Read-only access, always.

2. Add the workflow

Create .github/workflows/greenroom.yml with two jobs. The build job produces the artifact; the greenroom job consumes it. This example is React Native with CocoaPods; a native SwiftUI app just drops the node and pod steps:

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

permissions:
  contents: read

jobs:
  build:
    runs-on: macos-15
    timeout-minutes: 60
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with: { persist-credentials: false }
      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: pod install --project-directory=ios
      # Release, not Debug: the runner asserts an embedded JS bundle so the
      # simulator app never needs a Metro server.
      - name: Build the simulator artifact without secrets
        run: |
          set -euo pipefail
          xcodebuild \
            -workspace ios/YourApp.xcworkspace \
            -scheme YourApp \
            -configuration Release \
            -sdk iphonesimulator \
            -derivedDataPath "${RUNNER_TEMP}/DerivedData" \
            -destination "generic/platform=iOS Simulator" \
            CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO \
            build
      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: greenroom-app
          path: ${{ runner.temp }}/DerivedData/Build/Products/Release-iphonesimulator/YourApp.app

  greenroom:
    needs: build
    runs-on: macos-15
    timeout-minutes: 60
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
        with: { fetch-depth: 0, persist-credentials: false }
      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with: { name: greenroom-app, path: build/YourApp.app }
      - name: Materialize Greenroom policy from the trusted base revision
        shell: bash
        env:
          GREENROOM_BASE_SHA: ${{ github.event.pull_request.base.sha }}
        run: |
          set -euo pipefail
          git show "${GREENROOM_BASE_SHA}:.greenroom/environment.json" > "${RUNNER_TEMP}/greenroom-environment.json"
          git show "${GREENROOM_BASE_SHA}:.greenroom/state-contract.json" > "${RUNNER_TEMP}/greenroom-state-contract.json"
      # Pin this action to a reviewed commit SHA.
      - uses: justindc100/greenroom@REPLACE_WITH_REVIEWED_COMMIT_SHA
        with:
          greenroom-api-url: https://app.getgreenroom.io
          platform: ios
          artifact: build/YourApp.app
          app: com.example.yourapp
          device: Greenroom QA
          # Never trust isolation policy or coverage authority from PR code.
          environment-manifest: ${{ runner.temp }}/greenroom-environment.json
          state-contract: ${{ runner.temp }}/greenroom-state-contract.json
          base-sha: ${{ github.event.pull_request.base.sha }}
          head-sha: ${{ github.event.pull_request.head.sha }}
          workflow-ref: ${{ github.workflow_ref }}
          workflow-sha: ${{ github.workflow_sha }}
          pull-request-number: ${{ github.event.pull_request.number }}

Why the split matters: the build job runs your project's own scripts and dependency install, so it must never hold credentials. The greenroom job holds only the OIDC identity used to talk to Greenroom, and it runs a prebuilt artifact rather than your build system. See Security model.

3. Add the environment manifest

iOS accepts two network attestations, and the recommended one requires no infrastructure:

  • sandboxed_backend (recommended): your simulator build is compiled against a test or staging backend, so the app cannot reach production by construction. Greenroom's evidence records every network event during the walk, so an off-allowlist request is observable in the handoff even though egress is not blocked in-line.
  • external_proxy: for teams that already run an egress proxy or firewall on their CI hosts, the strictest option. The proxy denies everything outside allowedHosts.
{
  "schemaVersion": "1.0",
  "classification": "test",
  "isolated": true,
  "resetStrategy": "fresh_install",
  "networkControl": "sandboxed_backend",
  "allowedHosts": ["api.staging.example.test", "sentry.example.test"],
  "productionHosts": ["api.example.com"],
  "sandboxPurchases": true,
  "notes": "Release simulator build pinned to the staging tenant."
}

Two iOS-specific rules the schema enforces:

  • resetStrategy must be fresh_install. Every walk starts from a clean install of the artifact on a dedicated simulator, so state from one walk can never leak into the next.
  • networkControl: "playwright" is rejected for iOS; it belongs to web runs.

The general rules apply too: list every host including analytics and crash reporters, and never let a host appear in both allowedHosts and productionHosts. See the manifest reference.

4. Add the state contract

Same file as web, with an iOS router kind. For SwiftUI, graphPaths names your target's source root; for Expo Router, the app/ directory:

{
  "schemaVersion": "1.0",
  "routerKind": "swiftui",
  "graphPaths": ["Sources/YourApp"],
  "entryState": "today",
  "states": [
    { "id": "today", "screen": "TodayView", "goal": "Review today's plan" },
    { "id": "settings-profile", "screen": "ProfileView", "goal": "Update the profile height" }
  ]
}

Ship it at onboarding. Without a contract, coverage authority stays requested-goal-only and clean runs cap at inconclusive. See the state contract reference.

5. Open a pull request

The first run screens Greenroom's model roster against your actual app (about a dollar of model spend) to fix per-app routing; iOS apps differ enough that this screening measurably changes which models drive well. After that, every PR gets walked and receives its handoff.

On this page