Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ This is the log of notable changes to EAS CLI and related packages.

### 🐛 Bug fixes

- [eas-cli] Use development mode and remove inherited dotenv values when creating project fingerprints. ([#4246](https://github.com/expo/eas-cli/pull/4246) by [@ramonclaudio](https://github.com/ramonclaudio))

### 🧹 Chores

## [22.2.0](https://github.com/expo/eas-cli/releases/tag/v22.2.0) - 2026-08-20
Expand Down
169 changes: 169 additions & 0 deletions packages/eas-cli/src/fingerprint/__tests__/cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { createFingerprintAsync, createFingerprintsByKeyAsync } from '../cli';

const mockCreateFingerprintAsync = jest.fn();

jest.mock('resolve-from', () => ({
silent: jest.fn(() => 'expo/fingerprint'),
}));
jest.mock(
'expo/fingerprint',
() => ({
createFingerprintAsync: mockCreateFingerprintAsync,
}),
{ virtual: true }
);
jest.mock('../../ora', () => ({
ora: () => ({
start() {
return this;
},
succeed: jest.fn(),
fail: jest.fn(),
stop: jest.fn(),
}),
}));

describe('Fingerprint env', () => {
const originalEnv = process.env;

beforeEach(() => {
jest.clearAllMocks();
process.env = {
...originalEnv,
DOTENV_VALUE: 'from-parent',
PARENT_DOTENV_VALUE: 'from-parent',
FROM_PROCESS: 'true',
NODE_ENV: 'production',
__EXPO_ENV_LOADED: '["DOTENV_VALUE","PARENT_DOTENV_VALUE"]',
__EXPO_CONFIG_MODE: 'production',
};
});

afterEach(() => {
process.env = originalEnv;
});

it('removes inherited dotenv values and uses development mode', async () => {
const envBeforeFingerprint = process.env;
const envValuesBeforeFingerprint = { ...process.env };
const fingerprintEnv = {
DOTENV_VALUE: 'from-eas',
NODE_ENV: 'staging',
__EXPO_ENV_LOADED: '["DOTENV_VALUE"]',
__EXPO_CONFIG_MODE: 'staging',
};
const fingerprintEnvBefore = { ...fingerprintEnv };
mockCreateFingerprintAsync.mockImplementationOnce(async () => {
expect(process.env).toMatchObject({
DOTENV_VALUE: 'from-eas',
FROM_PROCESS: 'true',
NODE_ENV: 'development',
});
expect(process.env.PARENT_DOTENV_VALUE).toBeUndefined();
expect(process.env.__EXPO_ENV_LOADED).toBeUndefined();
expect(process.env.__EXPO_CONFIG_MODE).toBeUndefined();
return { hash: 'hash', sources: [] };
});

await createFingerprintAsync('/app', {
platforms: ['ios'],
env: fingerprintEnv,
});

expect(process.env).toBe(envBeforeFingerprint);
expect(process.env).toEqual(envValuesBeforeFingerprint);
expect(fingerprintEnv).toEqual(fingerprintEnvBefore);
});

it('runs fingerprints with different envs one at a time', async () => {
const envBeforeFingerprints = process.env;
let resolveIosFingerprint!: (value: { hash: string; sources: never[] }) => void;
let resolveAndroidFingerprint!: (value: { hash: string; sources: never[] }) => void;
mockCreateFingerprintAsync
.mockImplementationOnce(
() =>
new Promise(resolve => {
expect(process.env.FINGERPRINT_TARGET).toBe('ios');
resolveIosFingerprint = resolve;
})
)
.mockImplementationOnce(
() =>
new Promise(resolve => {
expect(process.env.FINGERPRINT_TARGET).toBe('android');
resolveAndroidFingerprint = resolve;
})
);

const fingerprintsPromise = createFingerprintsByKeyAsync(
'/app',
new Map([
['ios', { platforms: ['ios'], env: { FINGERPRINT_TARGET: 'ios' } }],
['android', { platforms: ['android'], env: { FINGERPRINT_TARGET: 'android' } }],
])
);

expect(mockCreateFingerprintAsync).toHaveBeenCalledTimes(1);
expect(process.env.FINGERPRINT_TARGET).toBe('ios');

resolveIosFingerprint({ hash: 'ios', sources: [] });
await new Promise(resolve => setImmediate(resolve));

expect(mockCreateFingerprintAsync).toHaveBeenCalledTimes(2);
expect(process.env.FINGERPRINT_TARGET).toBe('android');

resolveAndroidFingerprint({ hash: 'android', sources: [] });
await expect(fingerprintsPromise).resolves.toEqual(
new Map([
['ios', { hash: 'ios', sources: [] }],
['android', { hash: 'android', sources: [] }],
])
);
expect(process.env).toBe(envBeforeFingerprints);
});

it('keeps the env until parallel Fingerprint calls finish after a failure', async () => {
const envBeforeFingerprints = process.env;
let rejectFirstFingerprint!: (error: Error) => void;
let resolveSecondFingerprint!: (value: { hash: string; sources: never[] }) => void;
mockCreateFingerprintAsync
.mockImplementationOnce(
() =>
new Promise((_, reject) => {
rejectFirstFingerprint = reject;
})
)
.mockImplementationOnce(
() =>
new Promise(resolve => {
resolveSecondFingerprint = resolve;
})
);

const fingerprintsPromise = createFingerprintsByKeyAsync(
'/app',
new Map([
['ios', { platforms: ['ios'], env: undefined }],
['android', { platforms: ['android'], env: undefined }],
])
);
let didReject = false;
const rejectionPromise = fingerprintsPromise.catch(error => {
didReject = true;
throw error;
});

expect(mockCreateFingerprintAsync).toHaveBeenCalledTimes(2);
expect(process.env.NODE_ENV).toBe('development');

rejectFirstFingerprint(new Error('fingerprint failed'));
await new Promise(resolve => setImmediate(resolve));
expect(didReject).toBe(false);
expect(process.env.NODE_ENV).toBe('development');

resolveSecondFingerprint({ hash: 'android', sources: [] });
await expect(rejectionPromise).rejects.toThrow('fingerprint failed');

expect(process.env).toBe(envBeforeFingerprints);
});
});
72 changes: 60 additions & 12 deletions packages/eas-cli/src/fingerprint/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { silent as silentResolveFrom } from 'resolve-from';
import { Fingerprint, FingerprintDiffItem } from './types';
import Log from '../log';
import { ora } from '../ora';
import mapMapAsync from '../utils/expodash/mapMapAsync';
import { getEnvWithoutInheritedDotenvValues } from '../utils/originalEnv';

export type FingerprintOptions = {
workflow?: Workflow;
Expand Down Expand Up @@ -83,6 +83,20 @@ async function createFingerprintWithoutLoggingAsync(
Fingerprint & {
isDebugSource: boolean;
}
> {
return await withTemporaryEnvAsync(options.env ?? {}, () =>
createFingerprintWithCurrentEnvAsync(projectDir, fingerprintPath, options)
);
}

async function createFingerprintWithCurrentEnvAsync(
projectDir: string,
fingerprintPath: string,
options: FingerprintOptions
): Promise<
Fingerprint & {
isDebugSource: boolean;
}
> {
const Fingerprint = require(fingerprintPath);
const fingerprintOptions: Record<string, any> = {};
Expand All @@ -105,14 +119,18 @@ async function createFingerprintWithoutLoggingAsync(
}
fingerprintOptions.silent = true;

return await withTemporaryEnvAsync(options.env ?? {}, () =>
Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions)
);
return await Fingerprint.createFingerprintAsync(projectDir, fingerprintOptions);
}

async function withTemporaryEnvAsync(envVars: Env, fn: () => Promise<any>): Promise<any> {
const originalEnv = { ...process.env };
Object.assign(process.env, envVars);
async function withTemporaryEnvAsync<T>(envVars: Env, fn: () => Promise<T>): Promise<T> {
const originalEnv = process.env;
process.env = {
...getEnvWithoutInheritedDotenvValues(process.env),
...envVars,
NODE_ENV: 'development',
};
delete process.env.__EXPO_ENV_LOADED;
delete process.env.__EXPO_CONFIG_MODE;

try {
return await fn();
Expand Down Expand Up @@ -162,11 +180,41 @@ export async function createFingerprintsByKeyAsync(

const spinner = ora(`Computing project fingerprints`).start();
try {
const fingerprintsByKey = await mapMapAsync(
fingerprintOptionsByKey,
async options =>
await createFingerprintWithoutLoggingAsync(projectDir, fingerprintPath, options)
);
// Fingerprint reads process.env, so only calls that use the same env can run together.
const fingerprintOptionsByEnv = new Map<Env | undefined, [string, FingerprintOptions][]>();
for (const entry of fingerprintOptionsByKey.entries()) {
const env = entry[1].env;
const entries = fingerprintOptionsByEnv.get(env) ?? [];
entries.push(entry);
fingerprintOptionsByEnv.set(env, entries);
}

const fingerprintsByKey = new Map<
string,
Fingerprint & {
isDebugSource: boolean;
}
>();
for (const [env, entries] of fingerprintOptionsByEnv) {
const fingerprints = await withTemporaryEnvAsync(env ?? {}, async () => {
const fingerprintPromises = entries.map(
async ([key, options]) =>
[
key,
await createFingerprintWithCurrentEnvAsync(projectDir, fingerprintPath, options),
] as const
);
try {
return await Promise.all(fingerprintPromises);
} catch (error) {
await Promise.allSettled(fingerprintPromises);
throw error;
}
});
for (const [key, fingerprint] of fingerprints) {
fingerprintsByKey.set(key, fingerprint);
}
}
spinner.succeed(`Computed project fingerprints`);
return fingerprintsByKey;
} catch (e) {
Expand Down
Loading