Revyl Documentation

Builds / Remote builds

Configuration

Named build profiles, recipe fields, and build CLI flags

.revyl/config.yaml owns the build recipes used by both local and remote builds. revyl init writes a detected starting point for mobile projects. Web recipes are configured by hand; init does not guess their framework, package manager, preview host, or CI workflow. Edit the YAML directly, then run revyl config validate.

Direct builds require a project config with named profiles. If the selected file is legacy config, revyl build stops and directs you to preview and run revyl config migrate; it does not translate or rewrite the file implicitly.

Each invocation resolves exactly one named profile and one platform recipe:

revyl build --profile development --platform ios

The command runs in the cloud using the recipe's toolchain image and caches.

Cloud builds require authentication, a Git worktree, and a configured app_id UUID for an accessible app of the same platform. Cloud billing and capacity apply; failures never fall back to local execution.

revyl dev remains local unless you pass --remote; test/workflow --build continuations also remain local.

For Expo, the native prebuild recipes below run Xcode or Gradle directly and do not require Expo authentication. If you intentionally replace them with an EAS local-build command, install EAS CLI and add an encrypted EXPO_TOKEN reference for unattended remote builds.

Set up with an agent

Paste one prompt and your coding agent does the rest.

Setup prompt

Configure this project's Revyl build profiles.

Read https://docs.revyl.com/remote-builds/configuration.md and follow it. It's the current Revyl guide for this, so prefer it over anything you remember about Revyl.

Edit `.revyl/config.yaml` directly. Name profiles by intent, such as `development` and `pull-request`, each with a simulator or emulator recipe per platform. Put secret values in encrypted build secrets, never in the file: ask me to run `revyl build secret set <NAME> --stdin` myself.

You're done when `revyl config validate` passes.

Ask me before anything that needs my login or approval.

A complete example

build:
  framework: expo
  env:
    EXPO_PUBLIC_API_URL: https://staging.example.com
  profiles:
    development:
      ios:
        app_id: "<your-ios-app-uuid>"
        setup_commands:
          - bun install
          - bunx expo prebuild --platform ios
          - cd ios && pod install
        build_commands:
          - cd ios && xcodebuild -workspace YourApp.xcworkspace -scheme YourApp -configuration Release -sdk iphonesimulator -destination 'generic/platform=iOS Simulator' -derivedDataPath build ARCHS=arm64
        output_path: ios/build/Build/Products/Release-iphonesimulator/*.app
        image: ios-macos-26-xcode-26.5
      android:
        app_id: "<your-android-app-uuid>"
        setup_commands:
          - bun install
          - bunx expo prebuild --platform android
        build_commands:
          - cd android && ./gradlew assembleRelease
        output_path: android/app/build/outputs/apk/release/*.apk

Profiles and platform recipes

Profiles name a complete build intent such as development, pull-request, or release. Each profile may contain ios, android, and web recipes. There is no active or default profile stored in the file or on the server.

revyl build applies explicit --profile and --platform values first. When an omitted value has exactly one eligible choice, the CLI selects it. If multiple choices remain, an interactive terminal prompts and a non-interactive invocation fails with the valid choices and the flag to provide.

revyl build --profile development --platform ios
revyl build --profile release --platform android --remote

--platform accepts ios, android, or web; profile names are passed to --profile.

Build fields

build.framework is required when mobile recipes are present. Supported values are ios, android, react_native, expo, flutter, and web. Web-only profiles may omit it; their managed recipes use the Web framework.

Build-level env, secrets, and caches are inherited by every recipe. A recipe can add or override them as described below.

Recipe fieldDirect build requirementDescription
app_idRemote and non-interactive local: yes. Interactive local: optional before execution.UUID of the Revyl app that receives the finished build. After a successful interactive local build without one, Revyl lets you select or create an app and offers to save the binding into this recipe.
build_commandsYesOrdered shell commands or built-in steps, optionally named. Every declared recipe contains this field.
setup_commandsNoOrdered preparation commands or built-in steps run before build_commands, optionally named.
output_pathLocal and Web: yes. Remote mobile: optional.Artifact path or glob relative to the selected project root. Web requires one dedicated output directory without globs. When absent for remote mobile builds, Revyl uses the platform's existing default artifact glob.
imageNoToolchain image used for remote execution.
timeout_secondsNoPositive timeout for recipe execution.
envNoNon-secret environment values merged over build.env.
secretsNoSecret reference names merged with build.secrets.
cachesNoConfigured path caches merged with build.caches by key. See Caching.
runWeb onlyRuntime command, port, healthcheck, startup timeout, and organization variable names for the app container.

Web recipes

Web is in alpha. To add it to your organization, email support@revyl.ai.

For agent-led setup, follow the CLI's manual Web setup guide, including Next.js, static Vite, and test-only sign-in examples.

build:
  profiles:
    preview:
      web:
        app_id: "<your-web-app-uuid>"
        setup_commands: [npm ci]
        build_commands: [npm run build]
        output_path: dist

Use your package's own package manager and scripts. Run revyl build --profile preview --platform web for a managed remote build, or add --local to execute the same recipe on your machine and upload its output. An app-ID-only (URL-only) Web profile remains valid for registering deployment URLs, but cannot execute a managed build until commands and an output directory are set: revyl build and pr_review.build.kind: revyl reject it with profile <name> is URL-only. A Web profile needs app_id, a recipe, or both.

Web output is packed as .tar.zst containing only the declared directory's contents. Static output requires index.html; extensionless paths that match no file are served <path>.html or index.html, so client-side routes load directly. Server-rendered output includes the server and its runtime dependencies and sets run.start, relative to the bundle root:

output_path: dist-server
run:
  start: node server.js
  port: 3000
  healthcheck: /health
  start_timeout_seconds: 60
  env_keys: [APP_URL]
  secret_names: [API_TOKEN]

Bind the server to all interfaces (0.0.0.0) at the configured port, not only localhost or 127.0.0.1: the browser reaches the app at http://revyl-app.localhost:<port>. Revyl sets HOSTNAME=0.0.0.0 and PORT for your process, so a server that reads them needs no change. With no start, a static server is used. The app is ready once GET <healthcheck> returns a 2xx or 3xx status. Revyl checks the app through its sidecar hostname at the same port, so a localhost-only app fails the health check with couldn't connect to revyl-app:<port>. Defaults are port 3000, healthcheck /, and startup timeout 60 seconds (allowed range: 10–600). env_keys names plaintext organization launch variables and secret_names names encrypted organization launch variables. Only names belong here, never values. These variables are injected into the session's app process, not the browser or build. Build-time env and secrets keep their existing meaning.

Both compressed and unpacked bundles are limited to 200 MiB and 20,000 entries, with paths at most 1,024 bytes. Symlinks, special files, source-control state, and secret files such as .env* are rejected. Never put secrets in build output.

Mobile recipes permit app_id and output_path to be absent. A local build-and-upload requires output_path before running commands. A remote build may omit it and use the existing platform artifact-discovery default, but it requires app_id before submission. An interactive local build may run without app_id; after producing its artifact, the existing app-selection flow selects or creates the destination and offers to save that app ID into the selected recipe. Non-interactive local builds require the binding before running commands.

All setup and build commands run in order from the project root selected by the nearest .revyl/config.yaml (or by -C). Selecting a recipe never rewrites its commands, publishes project configuration, or registers saved-profile identity with the ordinary build API. The optional post-build local app binding is the only config write in this flow; remote builds require the binding up front and do not select, create, or save an app.

Named build steps

Add name to a command in either list to label it in the remote build's Steps tab:

setup_commands:
  - name: Install dependencies
    run: npm ci
build_commands:
  - name: Build Android APK
    run: cd android && ./gradlew assembleDebug

Names are optional. Plain command strings still work, and built-in steps can also have a name.

Inheritance

Build-level values are resolved into each recipe deterministically:

  • Recipe env entries replace build-level values with the same key.
  • Effective secrets are the ordered, de-duplicated build-level names followed by recipe names.
  • Caches merge by key. Paths are the ordered, de-duplicated build-level paths followed by recipe paths.
  • A name cannot be both an effective environment key and a secret reference.

Artifact resolution

After the last build command exits successfully, a local build resolves its required output_path. A remote build uses its authored output_path when present, or falls back to the existing platform default:

  • iOS: build/**/*.app; the result must be a simulator .app bundle or a supported authored archive containing one.
  • Android: **/build/outputs/apk/**/*.apk; .aab artifacts are unsupported. Use an .apk.
  • Web: the explicitly declared output directory; no default path or glob is inferred.

Use an explicit path or glob that identifies one artifact. Avoid broad globs over cached output directories that can also match stale products.

Environment variables

Non-secret values are exported into the selected recipe's setup and build commands. Use them for public build configuration such as EXPO_PUBLIC_* or ENVFILE:

build:
  framework: ios
  env:
    NODE_ENV: development
  profiles:
    development:
      ios:
        build_commands: ["./scripts/build-ios"]
        output_path: build/App.app
        env:
          EXPO_PUBLIC_API_URL: https://staging.example.com

Override or add values for one remote invocation:

revyl build --profile development --platform ios --remote \
  --env EXPO_PUBLIC_API_URL=https://preview.example.com

--env is a remote-only flag. Its values take precedence over the inherited config environment. Local builds use the inherited config environment as authored. Do not put credentials or tokens in env or --env; use secret references instead.

Compilation cache controls

These reserved environment settings control automatic compilation caching:

SettingPlatformValues
REVYL_XCODE_COMPILATION_CACHE_ENABLEDiOS"0" or "1"
REVYL_GRADLE_BUILD_CACHE_ENABLEDAndroid"0" or "1"
REVYL_CCACHE_ENABLEDAndroid"0" or "1"

All three default to "1". revyl build --remote --no-cache forces them off for one remote build and also skips configured path caches. See Caching.

Do not put credentials or tokens in env or --env; those values are part of the remote build request. Use encrypted build secrets instead.

Build secrets

Create or update an encrypted organization secret through the masked prompt:

revyl build secret set EXPO_TOKEN

For CI, send the value over stdin so it does not appear in process arguments or shell history:

printf '%s' "$EXPO_TOKEN" | revyl build secret set EXPO_TOKEN --stdin

Commit only the reference name:

build:
  framework: ios
  secrets:
    - EXPO_TOKEN
  profiles:
    development:
      ios:
        build_commands: ["./scripts/build-ios"]
        output_path: build/App.app
        secrets:
          - SENTRY_AUTH_TOKEN

Add references for one invocation with repeatable --secret flags:

revyl build --profile development --platform ios --remote \
  --secret EXPO_TOKEN --secret SENTRY_AUTH_TOKEN

Remote builds validate the names, decrypt the stored values only when the sandbox starts, and inject them into every setup and build command. Local builds read the same effective names from the current process environment and fail before running commands if a referenced value is missing.

Revyl does not parse .env.local automatically. Keep it out of version control. List or delete stored names without revealing their values:

revyl build secret list
revyl build secret delete EXPO_TOKEN

Private dependencies

Upload credentials as build secrets and configure authentication before the command that downloads private dependencies. The same setup commands run for CLI uploads and GitHub-triggered builds.

Private Git dependencies and submodules

For HTTPS Git downloads, add an entry to $HOME/.netrc. Choose the example for your Git host. Create the secret with the masked prompt:

revyl build secret set GITHUB_TOKEN
# For GitLab instead:
revyl build secret set GITLAB_TOKEN

Select the name under build.secrets (or the recipe's secrets):

secrets:
  - GITHUB_TOKEN

For GitLab, select GITLAB_TOKEN instead. Use a token with read access to each dependency repository. A GitLab personal access token needs read_repository; a deploy token needs its assigned username instead of oauth2 below.

Add the matching setup command to the recipe before dependency installation. These examples add a new host without erasing entries for other services. If that host already has an entry, update it instead of appending a duplicate.

GitHub:

setup_commands:
  - name: Configure GitHub credentials
    run: |
      set -eu
      set +x
      umask 077
      touch "$HOME/.netrc"
      chmod 600 "$HOME/.netrc"
      printf '\nmachine github.com\nlogin x-access-token\npassword %s\n' \
        "$GITHUB_TOKEN" >> "$HOME/.netrc"

GitLab:

setup_commands:
  - name: Configure GitLab credentials
    run: |
      set -eu
      set +x
      umask 077
      touch "$HOME/.netrc"
      chmod 600 "$HOME/.netrc"
      printf '\nmachine gitlab.com\nlogin oauth2\npassword %s\n' \
        "$GITLAB_TOKEN" >> "$HOME/.netrc"

These entries authenticate subsequent HTTPS Git commands to the matching host. SSH URLs such as git@github.com:company/sdk.git require SSH authentication. Submodules are separate repositories recorded in .gitmodules; with a Git checkout available on the runner, download them after configuring credentials:

git submodule update --init --recursive

Xcode Swift Package dependencies are a separate mechanism. To use system Git's authentication when resolving their Git repositories, use xcodebuild -resolvePackageDependencies -scmProvider system with your project's workspace/project and scheme arguments. Ensure Xcode is actually using system Git: an explicit IDEPackageSupportUseBuiltinSCM=false preference can prevent this on Xcode 26.3. This does not configure authentication for package registries or binary artifact downloads.

Private npm packages

Upload and select an npm token:

revyl build secret set NPM_TOKEN
secrets:
  - NPM_TOKEN
setup_commands:
  - npm ci

Add this literal line to the project's .npmrc, preserving existing settings:

//registry.npmjs.org/:_authToken=${NPM_TOKEN}

Commit the variable reference, never the token value. npm reads NPM_TOKEN from the build environment. For another registry, configure its hostname and any required package scope in .npmrc.

Using SSH instead: Store a dedicated SSH private key as a build secret and configure SSH before fetching Git dependencies. Register the public key with the Git host, use SSH repository URLs, and configure verified host keys in ~/.ssh/known_hosts. npm registry downloads still need registry credentials.

Remote source

revyl build archives the active Git worktree, including tracked files with current edits and nonignored untracked files, and sends the selected project directory as the runner workdir. This preserves workspace files and shared packages for nested monorepo projects while commands and artifact paths remain relative to the project containing .revyl/config.yaml. It does not publish the config or send saved-profile identity. The profile name and platform remain invocation-local selection inputs.

Deleted tracked files are omitted. Never track secrets, and add private runtime files to .gitignore. Git ignore rules do not exclude files that are already tracked. Use encrypted organization secrets rather than including their values in the source or build configuration.

Speed up uploads for large repositories

For monorepos or large repositories, use .revylignore to leave files the build does not need out of the source archive. This reduces upload size and time:

/docs/
/design-assets/

Place .revylignore in the project root containing .revyl/config.yaml, or at the Git worktree root. The project file takes precedence when both exist. Patterns with a leading / are relative to the Git worktree root, even for a nested project. For example, use /apps/mobile/docs/ for apps/mobile/docs/.

CLI reference

revyl build [--profile <name>] [--platform ios|android|web] [flags]
FlagDescription
--profile <name>Select a named local profile. Required when profile selection remains ambiguous.
--platform ios|android|webSelect the recipe inside the profile. Required when platform selection remains ambiguous.
--localBuild on your machine and upload the result.
--remoteRun on a Revyl cloud runner, the default. Retained for compatibility; --remote=false selects local execution.
--image <key>Override the selected recipe's remote toolchain image.
--env KEY=VALUERemote environment override, repeatable.
--secret NAMESecret reference, repeatable. Local builds read it from the process environment; remote builds use the stored organization secret.
--timeout <seconds>Override the selected recipe's remote timeout.
--version <string>Version label for the registered build. Auto-generated when omitted; does not select a branch.
--branch <name>Revyl branch to upload to. Without it, the Git branch of the source it submits, otherwise the app's default branch.
--detachQueue a remote build and return the job ID without waiting.
--no-cacheRun a cold remote build without compilation caches or configured path caches.
--pinPin the branch to the new build. Uploads don't pin otherwise; see Build branches.
--jsonWrite the remote-build JSON envelope by default. --local --json preserves the local build/upload JSON format.
--debugStream detailed diagnostics and remote build logs.

Do not combine --local with --remote. --image, --env, --timeout, --detach, and --no-cache are cloud-only.

Managing running remote builds:

revyl build status <build-job-id>
revyl build status <build-job-id> --follow
revyl build cancel <build-job-id>
revyl build list

Timeout

Remote builds are stopped by the server when they exceed their timeout: 60 minutes by default, up to a maximum of four hours. Set a recipe-specific value:

build:
  framework: ios
  profiles:
    release:
      ios:
        build_commands: ["./scripts/build-release"]
        output_path: build/Release.app
        timeout_seconds: 3600

Or override it for one invocation:

revyl build --profile release --platform ios --remote --timeout 3600

Ctrl-C stops following the build; the build keeps running in the cloud. Cancel it with revyl build cancel <build-job-id>.

On this page