.easignore next to eas.json is ignored, the git root one is used instead
- Dominant language
- TypeScript
- Stars
- 1.4k
- Forks
- 236
- Avg merge
- 3d 1h
- Merged PRs (30d)
- 91
Description
### 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](https://github.com/expo/fyi/blob/main/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:
```json
{
"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:
```ts
// resolve .easignore next to eas.json, fall back to the git root
async function resolveEasignorePathAsync(projectDir: string, rootPath: string): Promise {
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:
1. **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.
2. **should it warn?** a `Log.warn` when both file exist would make the precedence visible and reduce the risk from the point above.
3. **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.
Contributor guide
Research direction
Start with NoVcsClient.getRootPathAsync(), the Ignore path resolution, and the corresponding git and no-VCS client call sites described in the issue. Reproduce from a monorepo with separate app-local .easignore files, then verify that the project-directory file is selected without changing the existing root path or pattern semantics. Confirm both clients use the agreed precedence and that the archive ignores the expected files.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- git, typescript
- Domain
- build-system, cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 65/100