Build/Submit details page URL
No response
Summary
Summary
with EAS_NO_VCS=1 and no EAS_PROJECT_ROOT set, the docs say .easignore is read from the same directory as your eas.json, but eas-cli read it from the git root instead. so the documented location is silently ignored inside any git repo.
eas-build-archive.md say:
If you want to use .easignore instead of .gitignore with EAS_NO_VCS=1, then you need to place it in the directory pointed to by the EAS_PROJECT_ROOT environment variable (if it's set), or in the same directory as your eas.json otherwise.
that last part dont happen. NoVcsClient.getRootPathAsync() only fallback to the current directory when git rev-parse --show-toplevel throw. inside a git repo it never throw, so the root is the git root, and Ignore join .easignore onto that. the file next to eas.json is never read. no warning, no debug log.
i think this come from #2904 (merged march 2025) which put the git root lookup before process.cwd(). the doc was not updated after.
expected: .easignore next to eas.json is used when EAS_PROJECT_ROOT is not set, like the doc say.
actual: it is ignored, and the git root .easignore (or the .gitignore set) is used.
Current behaviour
both vcs client resolve .easignore by joining the filename onto the git root path, and nothing look at the project dir. that same path is used for the requireCommit warning, for the prune after clone in the git client, for the ignore check in isFileIgnoredAsync, and for building the ignore mapping in the no-vcs client.
so if you put .easignore inside an app directory it just do nothing. no warning, no debug log, silent.
Why this is a problem
1. multiple app in one repo cannot have their own ignore list.
one root filename, N apps. whatever you put there is wrong for at least N-1 of them.
2. .easignore replace every .gitignore instead of merging them.
when an .easignore exist, no .gitignore is read at all. so the moment one app in the repo need an .easignore, that one root file become the full ignore spec for every app in the repo. there is no way to opt in one app without taking over the others.
this is not only theory, people already refuse the file for this reason. in #3714 @Nezz say it directly: "Note that we don't want to use .easignore because want to rely on the .gitignore instead. Otherwise we'd need to duplicate our ignores between the two files."
3. the workaround is a race.
the only thing that work today is copy the per app file to the root before every eas command:
{
"scripts": {
"build:prod": "cp .easignore ../../.easignore && eas build --profile production"
}
}
two eas job running at the same time in the same checkout will overwrite each other root .easignore. in ci this is a real correctness problem, not only ergonomic. the archive you upload depend on which job win the write.
4. the documented escape hatch need you to disable vcs completely.
the workaround people land on in #2938 is EAS_NO_VCS=1 EAS_PROJECT_ROOT=/absolute/path/to/app eas build. that throw away the git packing path, and everything requireCommit give you, only to move one file.
Why this should be a small change
where the file live and what the pattern is relative to are already two separate things. so moving the lookup does not change any pattern semantic.
in the git client the path is handed to git ls-files --exclude-from. that flag take any absolute path and anchor the pattern to the repo it is listing, not to where the file sit. in the no-vcs client the file content is read and passed to the ignore package with an empty prefix, and it is the prefix, not the file location, that anchor the matching to the root dir.
so the change is only to resolve a different path and leave the root dir and all the prefix alone. roughly:
// resolve .easignore next to eas.json, fall back to the git root
async function resolveEasignorePathAsync(projectDir: string, rootPath: string): Promise<string> {
const projectEasignorePath = path.join(projectDir, EASIGNORE_FILENAME);
if (await fs.exists(projectEasignorePath)) {
return projectEasignorePath;
}
return path.join(rootPath, EASIGNORE_FILENAME);
}
four call site, all keyed off the same root path today. both client already carry the project directory internally, so no new plumbing needed.
Open questions for maintainers
i am not sure on these, happy to go whichever way you prefer:
- precedence when both exist. app win, root win, or throw error? app win is what i would expect, but that is a silent behaviour change for anyone who have a stray
.easignore in an app dir that is inert until now. probably rare, but not zero.
- should it warn? a
Log.warn when both file exist would make the precedence visible and reduce the risk from the point above.
- scope. the git client and the no-vcs client both need the same treatment. the no-vcs path already have
EAS_PROJECT_ROOT and absolute path handling, so maybe it want a slightly different shape.
Prior art
there is a few issue that i think all come back to this, but nobody frame it as a lookup problem.
#2938 is filed as an EAS_PROJECT_ROOT regression, and five different people land on the same workaround across about six month:
- @2ico say it plainly: "Moreover, .easignore in the expo directory is ignored. I have to use
EAS_NO_VCS=1 EAS_PROJECT_ROOT=... eas build -p ios for .easignore to work."
- @vonkanehoffen get ~200MB archive, workaround was delete the root
.git and git init inside the app dir, then ask "Surely there's gotta be a better way other than setting env vars for this?"
- @chmaltsp: "For me it only works with EAS_NO_VCS set to true and an absolute path."
- @heidgert on 16.17.4 run
eas build:inspect --stage archive and get the whole repo, same workaround, and say "it feels unnecessary to upload several hundreds of megabytes extra for no reason."
#3714 is the other side of the same thing. @Nezz have a 355 MB archive from eas metadata:pull screenshot, the cli itself print the hint to use .easignore, and they refuse it because it would mean duplicating every .gitignore (quoted above). so the one escape hatch is too blunt to take.
#4122 is a sibling. the copy step dont respect .git/info/exclude or a global core.excludesFile, so worktree and lfs binary get uploaded. different mechanism but same shape, the ignore input is not where the user expect it to be.
#3198 is a different .easignore bug about trailing slash directory matching. i mention it only so it is clear i am not asking to change that. my proposal dont touch pattern matching at all.
i cannot find an existing issue that state the lookup problem directly, so i open this one. sorry if i missed it.
to be clear about scope: this dont change how any pattern is matched, and it dont change what the pattern is relative to. same ignore behaviour, same root dir, same anchoring. the only difference is which file get read.
happy to open a PR if the approach and the precedence question above look right to you.
Managed or bare?
Managed
Environment
Environment
- eas-cli: v21.8.0 (behaviour checked against
main at time of writing)
- pnpm workspace monorepo, Expo SDK 54
npx expo-env-info
expo-env-info 2.1.0 environment info:
System:
OS: macOS 26.4
Shell: 5.9 - /bin/zsh
Binaries:
Node: 20.19.2 - /Users/fdesk/.local/share/mise/installs/node/20.19.2/bin/node
Yarn: 1.22.22 - /Users/fdesk/.local/share/mise/installs/yarn/1.22.22/bin/yarn
npm: 10.8.2 - /Users/fdesk/.local/share/mise/installs/node/20.19.2/bin/npm
Watchman: 2026.05.04.00 - /opt/homebrew/bin/watchman
Managers:
CocoaPods: 1.16.2 - /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms: DriverKit 25.2, iOS 26.2, macOS 26.2, tvOS 26.2, visionOS 26.2, watchOS 26.2
IDEs:
Android Studio: 2024.3 AI-243.26053.27.2432.13536105
Xcode: 26.3/17C529 - /usr/bin/xcodebuild
Expo Workflow: managed
Error output
No response
Reproducible demo or steps to reproduce from a blank project
Repro
monorepo, two expo app, pnpm workspaces:
repo/
├── apps/
│ ├── app-a/ # eas.json + .easignore
│ └── app-b/ # eas.json + .easignore
└── packages/
run eas build from apps/app-a. none of the .easignore is read, the archive is built from the .gitignore set instead. move either file to the repo root and it work, but now it control both app.
Build/Submit details page URL
No response
Summary
Summary
with
EAS_NO_VCS=1and noEAS_PROJECT_ROOTset, the docs say.easignoreis read from the same directory as youreas.json, but eas-cli read it from the git root instead. so the documented location is silently ignored inside any git repo.eas-build-archive.md say:
that last part dont happen.
NoVcsClient.getRootPathAsync()only fallback to the current directory whengit rev-parse --show-toplevelthrow. inside a git repo it never throw, so the root is the git root, andIgnorejoin.easignoreonto that. the file next toeas.jsonis never read. no warning, no debug log.i think this come from #2904 (merged march 2025) which put the git root lookup before
process.cwd(). the doc was not updated after.expected:
.easignorenext toeas.jsonis used whenEAS_PROJECT_ROOTis not set, like the doc say.actual: it is ignored, and the git root
.easignore(or the.gitignoreset) is used.Current behaviour
both vcs client resolve
.easignoreby joining the filename onto the git root path, and nothing look at the project dir. that same path is used for therequireCommitwarning, for the prune after clone in the git client, for the ignore check inisFileIgnoredAsync, and for building the ignore mapping in the no-vcs client.so if you put
.easignoreinside an app directory it just do nothing. no warning, no debug log, silent.Why this is a problem
1. multiple app in one repo cannot have their own ignore list.
one root filename, N apps. whatever you put there is wrong for at least N-1 of them.
2.
.easignorereplace every.gitignoreinstead of merging them.when an
.easignoreexist, no.gitignoreis read at all. so the moment one app in the repo need an.easignore, that one root file become the full ignore spec for every app in the repo. there is no way to opt in one app without taking over the others.this is not only theory, people already refuse the file for this reason. in #3714 @Nezz say it directly: "Note that we don't want to use
.easignorebecause want to rely on the.gitignoreinstead. Otherwise we'd need to duplicate our ignores between the two files."3. the workaround is a race.
the only thing that work today is copy the per app file to the root before every eas command:
{ "scripts": { "build:prod": "cp .easignore ../../.easignore && eas build --profile production" } }two eas job running at the same time in the same checkout will overwrite each other root
.easignore. in ci this is a real correctness problem, not only ergonomic. the archive you upload depend on which job win the write.4. the documented escape hatch need you to disable vcs completely.
the workaround people land on in #2938 is
EAS_NO_VCS=1 EAS_PROJECT_ROOT=/absolute/path/to/app eas build. that throw away the git packing path, and everythingrequireCommitgive you, only to move one file.Why this should be a small change
where the file live and what the pattern is relative to are already two separate things. so moving the lookup does not change any pattern semantic.
in the git client the path is handed to
git ls-files --exclude-from. that flag take any absolute path and anchor the pattern to the repo it is listing, not to where the file sit. in the no-vcs client the file content is read and passed to theignorepackage with an empty prefix, and it is the prefix, not the file location, that anchor the matching to the root dir.so the change is only to resolve a different path and leave the root dir and all the prefix alone. roughly:
four call site, all keyed off the same root path today. both client already carry the project directory internally, so no new plumbing needed.
Open questions for maintainers
i am not sure on these, happy to go whichever way you prefer:
.easignorein an app dir that is inert until now. probably rare, but not zero.Log.warnwhen both file exist would make the precedence visible and reduce the risk from the point above.EAS_PROJECT_ROOTand absolute path handling, so maybe it want a slightly different shape.Prior art
there is a few issue that i think all come back to this, but nobody frame it as a lookup problem.
#2938 is filed as an
EAS_PROJECT_ROOTregression, and five different people land on the same workaround across about six month:EAS_NO_VCS=1 EAS_PROJECT_ROOT=... eas build -p iosfor .easignore to work.".gitandgit initinside the app dir, then ask "Surely there's gotta be a better way other than setting env vars for this?"eas build:inspect --stage archiveand get the whole repo, same workaround, and say "it feels unnecessary to upload several hundreds of megabytes extra for no reason."#3714 is the other side of the same thing. @Nezz have a 355 MB archive from
eas metadata:pullscreenshot, the cli itself print the hint to use.easignore, and they refuse it because it would mean duplicating every.gitignore(quoted above). so the one escape hatch is too blunt to take.#4122 is a sibling. the copy step dont respect
.git/info/excludeor a globalcore.excludesFile, so worktree and lfs binary get uploaded. different mechanism but same shape, the ignore input is not where the user expect it to be.#3198 is a different
.easignorebug about trailing slash directory matching. i mention it only so it is clear i am not asking to change that. my proposal dont touch pattern matching at all.i cannot find an existing issue that state the lookup problem directly, so i open this one. sorry if i missed it.
to be clear about scope: this dont change how any pattern is matched, and it dont change what the pattern is relative to. same
ignorebehaviour, same root dir, same anchoring. the only difference is which file get read.happy to open a PR if the approach and the precedence question above look right to you.
Managed or bare?
Managed
Environment
Environment
mainat time of writing)Error output
No response
Reproducible demo or steps to reproduce from a blank project
Repro
monorepo, two expo app, pnpm workspaces:
run
eas buildfromapps/app-a. none of the.easignoreis read, the archive is built from the.gitignoreset instead. move either file to the repo root and it work, but now it control both app.