expo / expo/eas-cli

EAS Build --local does not resolve environment variables from CI/CD provider (GitHub Actions)

Open
#3,248 3 comments 0 reactions 1 assignee Claimed by @sjchmiela View on GitHub
needs review
Dominant language
TypeScript
Stars
1.4k
Forks
236
Avg merge
3d 8h
Merged PRs (30d)
98

Description

### Build/Submit details page URL

https://github.com/amehmeto/TiedSiren51/actions/runs/19090423640/job/54539925678

### Summary

When running eas build --local inside a GitHub Actions workflow, environment variables defined in eas.json with the $VARIABLE_NAME syntax are not resolved from the GitHub Actions environment. Instead, the literal string "$VARIABLE_NAME" is passed to the build environment, causing build failures.
This creates a significant limitation for developers who want to use local builds in CI for testing (PRs) and cloud builds for production (merges).

### Managed or bare?

Managed

### Environment

expo-env-info 2.0.7 environment info:
System:
OS: macOS 14.6.1
Shell: 5.9 - /bin/zsh
Binaries:
Node: 18.18.2 - ~/Library/Caches/fnm_multishells/52698_1762110251287/bin/node
Yarn: 1.22.22 - ~/.yarn/bin/yarn
npm: 9.8.1 - ~/Library/Caches/fnm_multishells/52698_1762110251287/bin/npm
Watchman: 2025.02.17.00 - /usr/local/bin/watchman
Managers:
CocoaPods: 1.15.2 - /usr/local/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 24.2, iOS 18.2, macOS 15.2, tvOS 18.2, visionOS 2.2, watchOS 11.2
IDEs:
Android Studio: 2025.1 AI-251.27812.49.2514.14217341
Xcode: 16.2/16C5032a - /usr/bin/xcodebuild
npmPackages:
expo: ~51.0.39 => 51.0.39
expo-router: ~3.5.24 => 3.5.24
react: 18.2.0 => 18.2.0
react-dom: 18.2.0 => 18.2.0
react-native: 0.74.5 => 0.74.5
react-native-web: ~0.19.10 => 0.19.13
npmGlobalPackages:
eas-cli: 16.26.0
Expo Workflow: bare [I'm surprised it says that, I .gitignored android/ and ios/ dir, but I use custom expo modules]

env: load .env.local .env.development .env
env: export EXPO_PUBLIC_FIREBASE_API_KEY EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN EXPO_PUBLIC_FIREBASE_PROJECT_ID EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID EXPO_PUBLIC_FIREBASE_APP_ID EXPO_PUBLIC_FIREBASE_MEASUREMENT_ID EXPO_ROUTER_APP_ROOT
16/16 checks passed. No issues detected!

### Error output

Current behavior
GitHub Actions workflow:
yaml- name: Build Android (Local)
env:
GOOGLE_SERVICES_JSON: ${{ secrets.GOOGLE_SERVICES_JSON }}
run: eas build --platform android --local --profile production --non-interactive
eas.json:
json{
"build": {
"production": {
"env": {
"GOOGLE_SERVICES_JSON": "$GOOGLE_SERVICES_JSON"
}
}
}
}
Build logs:
To debug this issue, I added:

A eas-build-pre-install hook that logs what process.env.GOOGLE_SERVICES_JSON contains
A eas-build-post-install hook that uses cat to verify the generated file

[PRE_INSTALL_HOOK] Script 'eas-build-pre-install' is present in package.json, running it...
[PRE_INSTALL_HOOK] > node scripts/setup-google-services.cjs
[PRE_INSTALL_HOOK] === setup-google-services.js ===
[PRE_INSTALL_HOOK] Current directory: /tmp/runner/eas-build-local-nodejs/6541f039-b4a7-482b-80f4-5b89957ab2ee/build
[PRE_INSTALL_HOOK] Environment variables available: [
[PRE_INSTALL_HOOK] 'EAS_BUILD_GIT_COMMIT_HASH',
[PRE_INSTALL_HOOK] 'GOOGLE_SERVICES_JSON',
[PRE_INSTALL_HOOK] ...
[PRE_INSTALL_HOOK] ]
[PRE_INSTALL_HOOK] EXPO_ROUTER_APP_ROOT resolve to: $EXPO_ROUTER_APP_ROOT
[PRE_INSTALL_HOOK] ❌ GOOGLE_SERVICES_JSON is not resolved (still contains $)
[PRE_INSTALL_HOOK] Value: $GOOGLE_SERVICES_JSON
[PRE_INSTALL_HOOK] 🔨 Decoding GOOGLE_SERVICES_JSON...
[PRE_INSTALL_HOOK] Base64 length: 21
[PRE_INSTALL_HOOK] Base64 preview: $GOOGLE_SERVICES_JSON...
[PRE_INSTALL_HOOK] 📝 Writing to: /tmp/runner/eas-build-local-nodejs/6541f039-b4a7-482b-80f4-5b89957ab2ee/build/google-services.json
[PRE_INSTALL_HOOK] ✅ google-services.json created successfully
[PRE_INSTALL_HOOK] File size: 22 bytes
[PRE_INSTALL_HOOK] File content preview:
[PRE_INSTALL_HOOK] �,O�HD�%#�

[POST_INSTALL_HOOK] Script 'eas-build-post-install' is present in package.json, running it...
[POST_INSTALL_HOOK] > cat google-services.json
[POST_INSTALL_HOOK] �,O�HD�%#�
Analysis:

The variable GOOGLE_SERVICES_JSON exists in the environment (it's listed)
But its value is the literal string "$GOOGLE_SERVICES_JSON" (21 characters)
When the script tries to decode this as base64, it produces corrupted output (22 bytes of garbage)
The post-install hook confirms the file contains corrupted data
Interesting observation: EXPO_ROUTER_APP_ROOT also shows the same behavior (resolves to $EXPO_ROUTER_APP_ROOT), but this didn't break the build in previous attempts. This raises the question: at which stage are environment variables actually resolved, and why does GOOGLE_SERVICES_JSON behave differently or cause a failure when other unresolved variables don't?

Expected behavior
When eas build --local runs in a CI environment where environment variables are set (e.g., GOOGLE_SERVICES_JSON from GitHub Secrets), the $GOOGLE_SERVICES_JSON reference in eas.json should be resolved from the shell environment, not treated as a literal string.
Question for the Expo team: What is the recommended way to pass environment variables from a CI provider (GitHub Actions) to eas build --local? The current $VARIABLE syntax in eas.json doesn't work for local builds, even when the variable is set in the CI environment.
Why this matters
Many developers want to use different build strategies based on the development stage:
My use case (and likely many others):

Open PRs: Use eas build --local in GitHub Actions for fast iteration and testing

PRs are expected to fail often during development
No point wasting EAS Cloud build credits on builds that will likely fail
Faster feedback loop for developers

Merged PRs: Use eas build (cloud) for production builds

These builds are expected to succeed
Take advantage of EAS infrastructure for distribution
Worth using build credits for stable, release-ready code

### Reproducible demo or steps to reproduce from a blank project

Create a GitHub Actions workflow with a secret
Set up eas.json with "VARIABLE": "$VARIABLE" in the env section
Run eas build --local with the secret set as environment variable
Observe that the literal string "$VARIABLE" is passed instead of the value

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.