Skip to content

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

@amehmeto

Description

@amehmeto

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

Metadata

Metadata

Assignees

Labels

needs reviewIssue is ready to be reviewed by a maintainer

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions