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 iosThe 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/*.apkProfiles 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 field | Direct build requirement | Description |
|---|---|---|
app_id | Remote 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_commands | Yes | Ordered shell commands or built-in steps, optionally named. Every declared recipe contains this field. |
setup_commands | No | Ordered preparation commands or built-in steps run before build_commands, optionally named. |
output_path | Local 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. |
image | No | Toolchain image used for remote execution. |
timeout_seconds | No | Positive timeout for recipe execution. |
env | No | Non-secret environment values merged over build.env. |
secrets | No | Secret reference names merged with build.secrets. |
caches | No | Configured path caches merged with build.caches by key. See Caching. |
run | Web only | Runtime 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: distUse 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 assembleDebugNames 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
enventries replace build-level values with the same key. - Effective
secretsare 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.appbundle or a supported authored archive containing one. - Android:
**/build/outputs/apk/**/*.apk;.aabartifacts 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.comOverride 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:
| Setting | Platform | Values |
|---|---|---|
REVYL_XCODE_COMPILATION_CACHE_ENABLED | iOS | "0" or "1" |
REVYL_GRADLE_BUILD_CACHE_ENABLED | Android | "0" or "1" |
REVYL_CCACHE_ENABLED | Android | "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_TOKENFor 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 --stdinCommit 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_TOKENAdd references for one invocation with repeatable --secret flags:
revyl build --profile development --platform ios --remote \
--secret EXPO_TOKEN --secret SENTRY_AUTH_TOKENRemote 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_TOKENPrivate 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_TOKENSelect the name under build.secrets (or the recipe's secrets):
secrets:
- GITHUB_TOKENFor 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 --recursiveXcode 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_TOKENsecrets:
- NPM_TOKEN
setup_commands:
- npm ciAdd 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]| Flag | Description |
|---|---|
--profile <name> | Select a named local profile. Required when profile selection remains ambiguous. |
--platform ios|android|web | Select the recipe inside the profile. Required when platform selection remains ambiguous. |
--local | Build on your machine and upload the result. |
--remote | Run 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=VALUE | Remote environment override, repeatable. |
--secret NAME | Secret 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. |
--detach | Queue a remote build and return the job ID without waiting. |
--no-cache | Run a cold remote build without compilation caches or configured path caches. |
--pin | Pin the branch to the new build. Uploads don't pin otherwise; see Build branches. |
--json | Write the remote-build JSON envelope by default. --local --json preserves the local build/upload JSON format. |
--debug | Stream 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 listTimeout
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: 3600Or override it for one invocation:
revyl build --profile release --platform ios --remote --timeout 3600Ctrl-C stops following the build; the build keeps running in the cloud. Cancel
it with revyl build cancel <build-job-id>.
Related
- Caching: compilation and configured path caches
- Toolchain Images: available images and installed tools
- Artifact Requirements: supported
.appand.apkartifacts