mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c45fdfd7e | ||
|
|
b024609a5e | ||
|
|
8afc461064 | ||
|
|
d2ed1feb67 | ||
|
|
cc49343b39 | ||
|
|
10f9edc910 | ||
|
|
9d1a6b7796 | ||
|
|
d8cca2c8e8 | ||
|
|
fb818fce54 | ||
|
|
11263c23a0 | ||
|
|
80baa13ffa | ||
|
|
58982ee212 |
No files matched your search
+10
-4
@@ -8,15 +8,21 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [expo-cocoapods-proxy] Fix iOS worker tarball build failing on macOS Tahoe due to bundler incompatibility with RubyGems 4. ([#3824](https://github.com/expo/eas-cli/pull/3824) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [20.1.0](https://github.com/expo/eas-cli/releases/tag/v20.1.0) - 2026-06-05
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `eas update:embedded:view` command. ([#3810](https://github.com/expo/eas-cli/pull/3810) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `eas update:embedded:list` command. ([#3811](https://github.com/expo/eas-cli/pull/3811) by [@gwdp](https://github.com/gwdp))
|
||||
- [eas-cli] Add `eas update:embedded:delete` command. ([#3809](https://github.com/expo/eas-cli/pull/3809) by [@gwdp](https://github.com/gwdp))
|
||||
- [build-tools] Auto-upload embedded bundle after build when `EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE` is set. ([#3767](https://github.com/expo/eas-cli/pull/3767) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [20.0.0](https://github.com/expo/eas-cli/releases/tag/v20.0.0) - 2026-05-29
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
@@ -3,4 +3,6 @@ _.path = "./tools/bin"
|
||||
MISE_NODE_COREPACK = "true"
|
||||
|
||||
[tools]
|
||||
gcloud = "570.0.0"
|
||||
jq = "1.8.1"
|
||||
node = "20.19.4"
|
||||
@@ -77,7 +77,7 @@
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/repack-app": "~0.4.1",
|
||||
"@expo/repack-app": "~0.6.1",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/lodash": "^4.17.4",
|
||||
|
||||
@@ -51,6 +51,7 @@ describe('Datadog singleton', () => {
|
||||
build_phase: 'install_dependencies',
|
||||
platform: 'ios',
|
||||
result: 'success',
|
||||
target: 'build',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -70,7 +71,7 @@ describe('Datadog singleton', () => {
|
||||
target: { kind: 'jobRun', turtleJobRunId: 'job-run-id' },
|
||||
});
|
||||
|
||||
Datadog.distribution('eas.workflow.maestro_cli_version', 1, {
|
||||
Datadog.distribution('eas.maestro.install', 1, {
|
||||
maestro_version: '2.1.0',
|
||||
});
|
||||
|
||||
@@ -81,11 +82,12 @@ describe('Datadog singleton', () => {
|
||||
json: {
|
||||
metrics: [
|
||||
{
|
||||
name: 'eas.workflow.maestro_cli_version',
|
||||
name: 'eas.maestro.install',
|
||||
type: 'distribution',
|
||||
value: 1,
|
||||
tags: {
|
||||
maestro_version: '2.1.0',
|
||||
target: 'job_run',
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -120,6 +122,7 @@ describe('Datadog singleton', () => {
|
||||
tags: {
|
||||
event: 'find_artifacts_absolute_path_dry_run',
|
||||
status: 'match',
|
||||
target: 'build',
|
||||
},
|
||||
},
|
||||
headers: { Authorization: 'Bearer token-abc' },
|
||||
@@ -150,6 +153,7 @@ describe('Datadog singleton', () => {
|
||||
message: 'Gradle cache restored (hit)',
|
||||
tags: {
|
||||
cache_type: 'gradle',
|
||||
target: 'job_run',
|
||||
},
|
||||
},
|
||||
headers: { Authorization: 'Bearer token-abc' },
|
||||
@@ -184,7 +188,14 @@ describe('Datadog singleton', () => {
|
||||
expect.any(Error),
|
||||
expect.objectContaining({
|
||||
extras: {
|
||||
metrics: [{ name: 'eas.build.phase_duration', type: 'distribution', value: 1, tags: {} }],
|
||||
metrics: [
|
||||
{
|
||||
name: 'eas.build.phase_duration',
|
||||
type: 'distribution',
|
||||
value: 1,
|
||||
tags: { target: 'build' },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -198,7 +209,7 @@ describe('Datadog singleton', () => {
|
||||
target: { kind: 'jobRun', turtleJobRunId: 'job-run-id' },
|
||||
});
|
||||
|
||||
Datadog.distribution('eas.workflow.maestro_cli_version', 1);
|
||||
Datadog.distribution('eas.maestro.install', 1);
|
||||
await flushPromises();
|
||||
|
||||
expect(sentryCaptureMock).toHaveBeenCalledWith(
|
||||
@@ -207,7 +218,12 @@ describe('Datadog singleton', () => {
|
||||
expect.objectContaining({
|
||||
extras: {
|
||||
metrics: [
|
||||
{ name: 'eas.workflow.maestro_cli_version', type: 'distribution', value: 1, tags: {} },
|
||||
{
|
||||
name: 'eas.maestro.install',
|
||||
type: 'distribution',
|
||||
value: 1,
|
||||
tags: { target: 'job_run' },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -231,7 +247,11 @@ describe('Datadog singleton', () => {
|
||||
expect.any(Error),
|
||||
expect.objectContaining({
|
||||
extras: {
|
||||
log: { buildId: 'build-id', message: 'artifact dry-run matched', tags: {} },
|
||||
log: {
|
||||
buildId: 'build-id',
|
||||
message: 'artifact dry-run matched',
|
||||
tags: { target: 'build' },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -253,7 +273,11 @@ describe('Datadog singleton', () => {
|
||||
expect.any(Error),
|
||||
expect.objectContaining({
|
||||
extras: {
|
||||
log: { jobRunId: 'job-run-id', message: 'Gradle cache restored (hit)', tags: {} },
|
||||
log: {
|
||||
jobRunId: 'job-run-id',
|
||||
message: 'Gradle cache restored (hit)',
|
||||
tags: { target: 'job_run' },
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { RuntimeSettings } from '../runtimeSettings';
|
||||
import { Sentry } from '../sentry';
|
||||
|
||||
jest.mock('node-fetch', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
jest.mock('../sentry', () => ({
|
||||
Sentry: {
|
||||
capture: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('runtimeSettings', () => {
|
||||
const logger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
};
|
||||
const originalCacheUrls = {
|
||||
EAS_NPM_CACHE_URL: process.env.EAS_NPM_CACHE_URL,
|
||||
EAS_NODEJS_CACHE_URL: process.env.EAS_NODEJS_CACHE_URL,
|
||||
EAS_MAVEN_CACHE_URL: process.env.EAS_MAVEN_CACHE_URL,
|
||||
EAS_COCOAPODS_CACHE_URL: process.env.EAS_COCOAPODS_CACHE_URL,
|
||||
};
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
afterEach(() => {
|
||||
jest.mocked(fetch).mockReset();
|
||||
jest.restoreAllMocks();
|
||||
jest.mocked(Sentry.capture).mockReset();
|
||||
logger.info.mockReset();
|
||||
logger.warn.mockReset();
|
||||
restoreEnv('EAS_NPM_CACHE_URL', originalCacheUrls.EAS_NPM_CACHE_URL);
|
||||
restoreEnv('EAS_NODEJS_CACHE_URL', originalCacheUrls.EAS_NODEJS_CACHE_URL);
|
||||
restoreEnv('EAS_MAVEN_CACHE_URL', originalCacheUrls.EAS_MAVEN_CACHE_URL);
|
||||
restoreEnv('EAS_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_COCOAPODS_CACHE_URL);
|
||||
mockProcessPlatform(originalPlatform);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(response({}));
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
jest.mocked(fetch).mockReset();
|
||||
logger.info.mockReset();
|
||||
logger.warn.mockReset();
|
||||
});
|
||||
|
||||
it('fetches runtime settings from the staging and production buckets', async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(response({}));
|
||||
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: 'staging',
|
||||
logger: logger as any,
|
||||
cacheUrlFallbacks: {
|
||||
npm: 'https://fallback-npm.example',
|
||||
},
|
||||
});
|
||||
await RuntimeSettings.loadAsync({ environment: 'production', logger: logger as any });
|
||||
await RuntimeSettings.loadAsync({ environment: 'test', logger: logger as any });
|
||||
|
||||
expect(fetch).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'https://storage.googleapis.com/eas-workflows-staging/runtime-settings.json',
|
||||
{
|
||||
signal: expect.anything(),
|
||||
}
|
||||
);
|
||||
expect(fetch).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'https://storage.googleapis.com/eas-workflows-production/runtime-settings.json',
|
||||
{
|
||||
signal: expect.anything(),
|
||||
}
|
||||
);
|
||||
expect(fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('does not use caches by default when remote settings are unavailable', async () => {
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
jest.mocked(fetch).mockResolvedValue(response({}, 404));
|
||||
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
expect(RuntimeSettings.isUsingIosPrecompiledModulesEnabled()).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalled();
|
||||
expect(Sentry.capture).toHaveBeenCalledWith('Failed to fetch worker runtime settings', {
|
||||
extras: {
|
||||
status: 404,
|
||||
url: 'https://storage.googleapis.com/eas-workflows-staging/runtime-settings.json',
|
||||
},
|
||||
level: 'warning',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not override existing settings when remote settings are invalid', async () => {
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
jest
|
||||
.mocked(fetch)
|
||||
.mockResolvedValueOnce(response({ caches: { linux: { npm: true } } }))
|
||||
.mockResolvedValueOnce(response({ iosPrecompiledModules: 'enabled' }));
|
||||
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBe('https://npm.example');
|
||||
expect(RuntimeSettings.isUsingIosPrecompiledModulesEnabled()).toBe(false);
|
||||
expect(logger.warn).toHaveBeenCalled();
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Failed to load worker runtime settings',
|
||||
expect.any(Error),
|
||||
{
|
||||
extras: {
|
||||
url: 'https://storage.googleapis.com/eas-workflows-staging/runtime-settings.json',
|
||||
},
|
||||
level: 'warning',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts partial runtime settings and ignores unknown fields', async () => {
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
process.env.EAS_NODEJS_CACHE_URL = 'https://node.example';
|
||||
process.env.EAS_MAVEN_CACHE_URL = 'https://maven.example';
|
||||
process.env.EAS_COCOAPODS_CACHE_URL = 'https://pods.example';
|
||||
jest.mocked(fetch).mockResolvedValue(
|
||||
response({
|
||||
caches: {
|
||||
linux: { npm: false, nodejs: true, maven: true },
|
||||
darwin: { npm: true, maven: true, cocoapods: false },
|
||||
},
|
||||
iosPrecompiledModules: true,
|
||||
unknownFutureSetting: true,
|
||||
})
|
||||
);
|
||||
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
expect(RuntimeSettings.getNodeJsCacheUrl()).toBe('https://node.example');
|
||||
expect(RuntimeSettings.getMavenCacheUrl()).toBe('https://maven.example');
|
||||
mockProcessPlatform('darwin');
|
||||
expect(RuntimeSettings.getMavenCacheUrl()).toBe('https://maven.example');
|
||||
expect(RuntimeSettings.getCocoapodsCacheUrl()).toBeNull();
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBe('https://npm.example');
|
||||
expect(RuntimeSettings.isUsingIosPrecompiledModulesEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('uses Maven and CocoaPods caches when enabled for the current platform and their environment variables exist', async () => {
|
||||
process.env.EAS_MAVEN_CACHE_URL = 'https://maven.example';
|
||||
process.env.EAS_COCOAPODS_CACHE_URL = 'https://pods.example';
|
||||
jest.mocked(fetch).mockResolvedValue(
|
||||
response({
|
||||
caches: {
|
||||
linux: { maven: true },
|
||||
darwin: { maven: true, cocoapods: true },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getMavenCacheUrl()).toBe('https://maven.example');
|
||||
mockProcessPlatform('darwin');
|
||||
expect(RuntimeSettings.getMavenCacheUrl()).toBe('https://maven.example');
|
||||
expect(RuntimeSettings.getCocoapodsCacheUrl()).toBe('https://pods.example');
|
||||
});
|
||||
|
||||
it('uses cache URL fallbacks when enabled cache environment variables are not set', async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(
|
||||
response({
|
||||
caches: {
|
||||
linux: { npm: true, nodejs: true, maven: true },
|
||||
darwin: { cocoapods: true },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: 'staging',
|
||||
logger: logger as any,
|
||||
cacheUrlFallbacks: {
|
||||
npm: 'https://fallback-npm.example',
|
||||
nodejs: 'https://fallback-node.example',
|
||||
maven: 'https://fallback-maven.example',
|
||||
cocoapods: 'https://fallback-pods.example',
|
||||
},
|
||||
});
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBe('https://fallback-npm.example');
|
||||
expect(RuntimeSettings.getNodeJsCacheUrl()).toBe('https://fallback-node.example');
|
||||
expect(RuntimeSettings.getMavenCacheUrl()).toBe('https://fallback-maven.example');
|
||||
mockProcessPlatform('darwin');
|
||||
expect(RuntimeSettings.getCocoapodsCacheUrl()).toBe('https://fallback-pods.example');
|
||||
});
|
||||
|
||||
it('prefers cache URL environment variables over fallbacks', async () => {
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
jest.mocked(fetch).mockResolvedValue(
|
||||
response({
|
||||
caches: {
|
||||
linux: { npm: true },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: 'staging',
|
||||
logger: logger as any,
|
||||
cacheUrlFallbacks: {
|
||||
npm: 'https://fallback-npm.example',
|
||||
},
|
||||
});
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBe('https://npm.example');
|
||||
});
|
||||
|
||||
it('does not use cache URL fallbacks when runtime settings disable caches', async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(
|
||||
response({
|
||||
caches: {
|
||||
linux: { npm: false },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: 'staging',
|
||||
logger: logger as any,
|
||||
cacheUrlFallbacks: {
|
||||
npm: 'https://fallback-npm.example',
|
||||
},
|
||||
});
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it('clears cache URL fallbacks when loading without them', async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(response({ caches: { linux: { npm: true } } }));
|
||||
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: 'staging',
|
||||
logger: logger as any,
|
||||
cacheUrlFallbacks: {
|
||||
npm: 'https://fallback-npm.example',
|
||||
},
|
||||
});
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
mockProcessPlatform('linux');
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not use caches when runtime settings are not loaded', () => {
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
|
||||
jest.isolateModules(() => {
|
||||
const { RuntimeSettings } = require('../runtimeSettings');
|
||||
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
expect(RuntimeSettings.isUsingIosPrecompiledModulesEnabled()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('reports when loaded runtime settings omit the current platform cache settings', async () => {
|
||||
jest.mocked(fetch).mockResolvedValue(response({ caches: { linux: { npm: true } } }));
|
||||
mockProcessPlatform('darwin');
|
||||
|
||||
await RuntimeSettings.loadAsync({ environment: 'staging', logger: logger as any });
|
||||
|
||||
expect(RuntimeSettings.getNpmCacheUrl()).toBeNull();
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: 'Runtime settings are missing cache settings for platform "darwin"',
|
||||
}),
|
||||
{
|
||||
extras: {
|
||||
configuredPlatforms: ['linux'],
|
||||
platform: 'darwin',
|
||||
},
|
||||
level: 'error',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function mockProcessPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
}
|
||||
|
||||
function response(body: unknown, status = 200): any {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ExpoRunFormatter } from '@expo/xcpretty';
|
||||
import fg from 'fast-glob';
|
||||
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { Sentry } from '../../sentry';
|
||||
import { XcodeBuildLogger } from '../xcpretty';
|
||||
|
||||
jest.mock('@expo/spawn-async');
|
||||
jest.mock('@expo/xcpretty');
|
||||
jest.mock('fast-glob');
|
||||
jest.mock('../../sentry', () => ({
|
||||
Sentry: {
|
||||
setup: jest.fn(),
|
||||
capture: jest.fn(),
|
||||
flush: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe(XcodeBuildLogger, () => {
|
||||
let stdout: EventEmitter;
|
||||
let formatter: { pipe: jest.Mock; getBuildSummary: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
stdout = new EventEmitter();
|
||||
const spawnPromise = Object.assign(new Promise(() => {}), {
|
||||
child: { stdout, kill: jest.fn() },
|
||||
});
|
||||
jest.mocked(spawnAsync).mockReturnValue(spawnPromise as any);
|
||||
|
||||
formatter = {
|
||||
pipe: jest.fn(),
|
||||
getBuildSummary: jest.fn().mockReturnValue('summary'),
|
||||
};
|
||||
jest.mocked(ExpoRunFormatter.create).mockReturnValue(formatter as any);
|
||||
|
||||
jest.mocked(fg).mockResolvedValue(['build.log'] as any);
|
||||
});
|
||||
|
||||
async function startWatchingAsync(logger: ReturnType<typeof createMockLogger>): Promise<void> {
|
||||
const buildLogger = new XcodeBuildLogger(logger, '/project');
|
||||
await buildLogger.watchLogFiles('/logs');
|
||||
}
|
||||
|
||||
it('logs formatted lines', async () => {
|
||||
const logger = createMockLogger();
|
||||
await startWatchingAsync(logger);
|
||||
|
||||
formatter.pipe.mockReturnValue(['line1', 'line2']);
|
||||
stdout.emit('data', 'raw xcodebuild output');
|
||||
|
||||
expect(formatter.pipe).toHaveBeenCalledWith('raw xcodebuild output');
|
||||
expect(logger.info).toHaveBeenCalledWith('line1');
|
||||
expect(logger.info).toHaveBeenCalledWith('line2');
|
||||
expect(Sentry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('falls back to raw logs when the formatter throws', async () => {
|
||||
const logger = createMockLogger();
|
||||
await startWatchingAsync(logger);
|
||||
|
||||
formatter.pipe.mockImplementation(() => {
|
||||
throw new RangeError('Invalid array length');
|
||||
});
|
||||
stdout.emit('data', 'offending output');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith('offending output');
|
||||
});
|
||||
|
||||
it('reports the formatter error to Sentry only once', async () => {
|
||||
const logger = createMockLogger();
|
||||
await startWatchingAsync(logger);
|
||||
|
||||
const error = new RangeError('Invalid array length');
|
||||
formatter.pipe.mockImplementation(() => {
|
||||
throw error;
|
||||
});
|
||||
stdout.emit('data', 'offending output');
|
||||
stdout.emit('data', 'more offending output');
|
||||
|
||||
expect(logger.info).toHaveBeenCalledWith('offending output');
|
||||
expect(logger.info).toHaveBeenCalledWith('more offending output');
|
||||
expect(Sentry.capture).toHaveBeenCalledTimes(1);
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'xcpretty formatter failed to parse xcodebuild logs',
|
||||
error,
|
||||
{ extras: { data: 'offending output' } }
|
||||
);
|
||||
});
|
||||
});
|
||||
+18
-3
@@ -6,11 +6,14 @@ import fg from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { Sentry } from '../sentry';
|
||||
|
||||
const CHECK_FILE_INTERVAL_MS = 1000;
|
||||
|
||||
export class XcodeBuildLogger {
|
||||
private loggerError?: Error;
|
||||
private flushing: boolean = false;
|
||||
private reportedFormatterError: boolean = false;
|
||||
private logReaderPromise?: SpawnPromise<SpawnResult>;
|
||||
private logsPath?: string;
|
||||
|
||||
@@ -60,9 +63,21 @@ export class XcodeBuildLogger {
|
||||
this.logReaderPromise = spawnAsync('tail', ['-n', '+0', '-f', logsPath], { stdio: 'pipe' });
|
||||
assert(this.logReaderPromise.child.stdout, 'stdout is not available');
|
||||
this.logReaderPromise.child.stdout.on('data', (data: string) => {
|
||||
const lines = formatter.pipe(data.toString());
|
||||
for (const line of lines) {
|
||||
this.logger.info(line);
|
||||
try {
|
||||
const lines = formatter.pipe(data.toString());
|
||||
for (const line of lines) {
|
||||
this.logger.info(line);
|
||||
}
|
||||
} catch (err: any) {
|
||||
// The formatter can throw on unexpected xcodebuild output. Fall back to
|
||||
// raw logs instead of crashing the process with an uncaught exception.
|
||||
if (!this.reportedFormatterError) {
|
||||
this.reportedFormatterError = true;
|
||||
Sentry.capture('xcpretty formatter failed to parse xcodebuild logs', err, {
|
||||
extras: { data: data.toString().slice(-2000) },
|
||||
});
|
||||
}
|
||||
this.logger.info(data.toString());
|
||||
}
|
||||
});
|
||||
await this.logReaderPromise;
|
||||
@@ -14,6 +14,13 @@ type DatadogSetupOptions = {
|
||||
let setupOptions: DatadogSetupOptions | null = null;
|
||||
let pendingUploads: Promise<unknown>[] = [];
|
||||
|
||||
function withTargetTag(
|
||||
tags: Record<string, string>,
|
||||
target: MetricsTarget
|
||||
): Record<string, string> {
|
||||
return { ...tags, target: target.kind === 'build' ? 'build' : 'job_run' };
|
||||
}
|
||||
|
||||
export const Datadog = {
|
||||
setup(opts: DatadogSetupOptions | null): void {
|
||||
setupOptions = opts;
|
||||
@@ -29,7 +36,7 @@ export const Datadog = {
|
||||
name,
|
||||
type: 'distribution' as const,
|
||||
value,
|
||||
tags,
|
||||
tags: withTargetTag(tags, target),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -58,10 +65,11 @@ export const Datadog = {
|
||||
return;
|
||||
}
|
||||
const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
|
||||
const tagsWithTarget = withTargetTag(tags, target);
|
||||
const log =
|
||||
target.kind === 'build'
|
||||
? { buildId: target.turtleBuildId, message, tags }
|
||||
: { jobRunId: target.turtleJobRunId, message, tags };
|
||||
? { buildId: target.turtleBuildId, message, tags: tagsWithTarget }
|
||||
: { jobRunId: target.turtleJobRunId, message, tags: tagsWithTarget };
|
||||
const logsPath = target.kind === 'build' ? 'turtle-builds/logs' : 'turtle-job-runs/logs';
|
||||
|
||||
const uploadPromise = turtleFetch(new URL(logsPath, expoApiV2BaseUrl).toString(), 'POST', {
|
||||
|
||||
@@ -27,3 +27,4 @@ export * from './generic';
|
||||
|
||||
export { Datadog } from './datadog';
|
||||
export { Sentry } from './sentry';
|
||||
export * from './runtimeSettings';
|
||||
@@ -10,8 +10,8 @@ import type { Credentials } from './credentials/manager';
|
||||
import { createFastfileForResigningBuild } from './fastfile';
|
||||
import { createGymfileForArchiveBuild, createGymfileForSimulatorBuild } from './gymfile';
|
||||
import { isTVOS } from './tvos';
|
||||
import { XcodeBuildLogger } from './xcpretty';
|
||||
import { COMMON_FASTLANE_ENV } from '../common/fastlane';
|
||||
import { XcodeBuildLogger } from '../common/xcpretty';
|
||||
import { BuildContext, SkipNativeBuildError } from '../context';
|
||||
|
||||
export async function runFastlaneGym<TJob extends Ios.Job>(
|
||||
|
||||
@@ -51,7 +51,7 @@ async function resolvePrecompiledModulesPodInstallEnvAsync<TJob extends Ios.Job>
|
||||
return {};
|
||||
}
|
||||
|
||||
if (ctx.job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES !== '1') {
|
||||
if (ctx.env.EAS_USE_PRECOMPILED_MODULES !== '1') {
|
||||
return {};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { type bunyan } from '@expo/logger';
|
||||
import fetch from 'node-fetch';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Sentry } from './sentry';
|
||||
|
||||
const ENVIRONMENT_TO_RUNTIME_SETTINGS_URL: Record<string, string | undefined> = {
|
||||
staging: 'https://storage.googleapis.com/eas-workflows-staging/runtime-settings.json',
|
||||
production: 'https://storage.googleapis.com/eas-workflows-production/runtime-settings.json',
|
||||
};
|
||||
|
||||
const RuntimeSettingsSchema = z
|
||||
.object({
|
||||
caches: z.record(
|
||||
z.string(),
|
||||
z
|
||||
.object({
|
||||
npm: z.boolean(),
|
||||
nodejs: z.boolean(),
|
||||
maven: z.boolean(),
|
||||
cocoapods: z.boolean(),
|
||||
})
|
||||
.partial()
|
||||
),
|
||||
iosPrecompiledModules: z.boolean(),
|
||||
})
|
||||
.partial();
|
||||
|
||||
type CacheUrlFallbacks = {
|
||||
npm?: string | null;
|
||||
nodejs?: string | null;
|
||||
maven?: string | null;
|
||||
cocoapods?: string | null;
|
||||
};
|
||||
|
||||
let runtimeSettings: z.infer<typeof RuntimeSettingsSchema> = {};
|
||||
let cacheUrlFallbacks: CacheUrlFallbacks = {};
|
||||
|
||||
export namespace RuntimeSettings {
|
||||
export async function loadAsync({
|
||||
environment,
|
||||
logger,
|
||||
cacheUrlFallbacks: nextCacheUrlFallbacks = {},
|
||||
}: {
|
||||
environment: string;
|
||||
logger: bunyan;
|
||||
cacheUrlFallbacks?: CacheUrlFallbacks;
|
||||
}): Promise<void> {
|
||||
cacheUrlFallbacks = nextCacheUrlFallbacks;
|
||||
const url = ENVIRONMENT_TO_RUNTIME_SETTINGS_URL[environment];
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(1000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn(
|
||||
{ url, status: response.status },
|
||||
'Failed to fetch worker runtime settings, using defaults'
|
||||
);
|
||||
Sentry.capture('Failed to fetch worker runtime settings', {
|
||||
extras: { url, status: response.status },
|
||||
level: 'warning',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeSettings = RuntimeSettingsSchema.parse(await response.json());
|
||||
if (runtimeSettings.caches && !runtimeSettings.caches[process.platform]) {
|
||||
Sentry.capture(
|
||||
new Error(
|
||||
`Runtime settings are missing cache settings for platform "${process.platform}"`
|
||||
),
|
||||
{
|
||||
extras: {
|
||||
configuredPlatforms: Object.keys(runtimeSettings.caches),
|
||||
platform: process.platform,
|
||||
},
|
||||
level: 'error',
|
||||
}
|
||||
);
|
||||
}
|
||||
logger.info({ url, settings: runtimeSettings }, 'Loaded worker runtime settings');
|
||||
} catch (err) {
|
||||
logger.warn({ err, url }, 'Failed to load worker runtime settings, using defaults');
|
||||
Sentry.capture(
|
||||
'Failed to load worker runtime settings',
|
||||
err instanceof Error ? err : new Error(String(err)),
|
||||
{
|
||||
extras: { url },
|
||||
level: 'warning',
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isUsingIosPrecompiledModulesEnabled(): boolean {
|
||||
return runtimeSettings.iosPrecompiledModules ?? false;
|
||||
}
|
||||
|
||||
export function getNpmCacheUrl(): string | null {
|
||||
return runtimeSettings.caches?.[process.platform]?.npm
|
||||
? process.env.EAS_NPM_CACHE_URL || cacheUrlFallbacks.npm || null
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getNodeJsCacheUrl(): string | null {
|
||||
return runtimeSettings.caches?.[process.platform]?.nodejs
|
||||
? process.env.EAS_NODEJS_CACHE_URL || cacheUrlFallbacks.nodejs || null
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getMavenCacheUrl(): string | null {
|
||||
return runtimeSettings.caches?.[process.platform]?.maven
|
||||
? process.env.EAS_MAVEN_CACHE_URL || cacheUrlFallbacks.maven || null
|
||||
: null;
|
||||
}
|
||||
|
||||
export function getCocoapodsCacheUrl(): string | null {
|
||||
return runtimeSettings.caches?.[process.platform]?.cocoapods
|
||||
? process.env.EAS_COCOAPODS_CACHE_URL || cacheUrlFallbacks.cocoapods || null
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { createGlobalContextMock } from '../../../__tests__/utils/context';
|
||||
import { Datadog } from '../../../datadog';
|
||||
import { createInstallMaestroBuildFunction } from '../installMaestro';
|
||||
|
||||
jest.mock('@expo/turtle-spawn', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../datadog', () => ({
|
||||
Datadog: {
|
||||
distribution: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const mockedSpawn = jest.mocked(spawn);
|
||||
|
||||
describe('createInstallMaestroBuildFunction', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// `maestro --version` reports an installed version; `java -version` succeeds.
|
||||
// With no requested version and Maestro present, the step skips installation.
|
||||
mockedSpawn.mockImplementation((async (command: string) => ({
|
||||
stdout: command === 'maestro' ? '1.41.0\n' : '',
|
||||
})) as any);
|
||||
});
|
||||
|
||||
it('reports the detected Maestro version to Datadog on EAS Build VMs', async () => {
|
||||
const installMaestro = createInstallMaestroBuildFunction();
|
||||
const globalCtx = createGlobalContextMock();
|
||||
globalCtx.updateEnv({ EAS_BUILD_RUNNER: 'eas-build' });
|
||||
const step = installMaestro.createBuildStepFromFunctionCall(globalCtx, { callInputs: {} });
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(step.getOutputValueByName('maestro_version')).toBe('1.41.0');
|
||||
expect(Datadog.distribution).toHaveBeenCalledWith('eas.maestro.install', 1, {
|
||||
maestro_version: '1.41.0',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not report to Datadog outside EAS Build VMs', async () => {
|
||||
const installMaestro = createInstallMaestroBuildFunction();
|
||||
const step = installMaestro.createBuildStepFromFunctionCall(createGlobalContextMock(), {
|
||||
callInputs: {},
|
||||
});
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(Datadog.distribution).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('extracts the version when `maestro --version` prints an analytics notice', async () => {
|
||||
mockedSpawn.mockImplementation((async (command: string) => ({
|
||||
stdout:
|
||||
command === 'maestro'
|
||||
? 'Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS environment variable to any value before running Maestro.\n2.0.10\n'
|
||||
: '',
|
||||
})) as any);
|
||||
|
||||
const installMaestro = createInstallMaestroBuildFunction();
|
||||
const globalCtx = createGlobalContextMock();
|
||||
globalCtx.updateEnv({ EAS_BUILD_RUNNER: 'eas-build' });
|
||||
const step = installMaestro.createBuildStepFromFunctionCall(globalCtx, { callInputs: {} });
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(step.getOutputValueByName('maestro_version')).toBe('2.0.10');
|
||||
expect(Datadog.distribution).toHaveBeenCalledWith('eas.maestro.install', 1, {
|
||||
maestro_version: '2.0.10',
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the trailing version when the notice itself contains an earlier version-like string', async () => {
|
||||
mockedSpawn.mockImplementation((async (command: string) => ({
|
||||
stdout: command === 'maestro' ? 'Analytics schema v2.0.0 enabled.\n2.0.10\n' : '',
|
||||
})) as any);
|
||||
|
||||
const installMaestro = createInstallMaestroBuildFunction();
|
||||
const globalCtx = createGlobalContextMock();
|
||||
globalCtx.updateEnv({ EAS_BUILD_RUNNER: 'eas-build' });
|
||||
const step = installMaestro.createBuildStepFromFunctionCall(globalCtx, { callInputs: {} });
|
||||
|
||||
await step.executeAsync();
|
||||
|
||||
expect(step.getOutputValueByName('maestro_version')).toBe('2.0.10');
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { Datadog } from '../../datadog';
|
||||
|
||||
export function createInstallMaestroBuildFunction(): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
@@ -123,13 +125,24 @@ export function createInstallMaestroBuildFunction(): BuildFunction {
|
||||
|
||||
logger.info(`Maestro ${maestroVersionResult.value} is ready.`);
|
||||
outputs.maestro_version.set(maestroVersionResult.value);
|
||||
|
||||
Datadog.distribution('eas.maestro.install', 1, {
|
||||
maestro_version: maestroVersionResult.value,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function getMaestroVersion({ env }: { env: BuildStepEnv }): Promise<string> {
|
||||
const maestroVersion = await spawn('maestro', ['--version'], { stdio: 'pipe', env });
|
||||
return maestroVersion.stdout.trim();
|
||||
const { stdout } = await spawn('maestro', ['--version'], { stdio: 'pipe', env });
|
||||
// `maestro --version` can print an analytics notice to stdout before the version,
|
||||
// e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
|
||||
// Take the last version-looking token: the real version is printed after the notice, so
|
||||
// this stays correct even if the notice itself contains a version-like string. Keeps the
|
||||
// step output and the eas.maestro.install metric tag clean. Best-effort only: fall back to
|
||||
// the raw output if none is found, so we never fail the build over a version string.
|
||||
const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
|
||||
return versions?.at(-1) ?? stdout.trim();
|
||||
}
|
||||
|
||||
async function installMaestro({
|
||||
|
||||
@@ -57,6 +57,16 @@ export function createRepackBuildFunction(): BuildFunction {
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'ios_signing_use_source_app_entitlements',
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.BOOLEAN,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'ios_signing_app_entitlements_path',
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
required: false,
|
||||
}),
|
||||
BuildStepInput.createProvider({
|
||||
id: 'repack_version',
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
@@ -138,6 +148,12 @@ export function createRepackBuildFunction(): BuildFunction {
|
||||
iosSigningOptions: await resolveIosSigningOptionsAsync({
|
||||
job: stepsCtx.global.staticContext.job,
|
||||
logger: stepsCtx.logger,
|
||||
useAppEntitlements: inputs.ios_signing_use_source_app_entitlements.value as
|
||||
| boolean
|
||||
| undefined,
|
||||
entitlementsPath: inputs.ios_signing_app_entitlements_path.value as
|
||||
| string
|
||||
| undefined,
|
||||
}),
|
||||
logger: stepsCtx.logger,
|
||||
spawnAsync: repackSpawnAsync,
|
||||
@@ -275,9 +291,13 @@ export async function resolveAndroidSigningOptionsAsync({
|
||||
export async function resolveIosSigningOptionsAsync({
|
||||
job,
|
||||
logger,
|
||||
useAppEntitlements,
|
||||
entitlementsPath,
|
||||
}: {
|
||||
job: Job;
|
||||
logger: bunyan;
|
||||
useAppEntitlements?: boolean;
|
||||
entitlementsPath?: string;
|
||||
}): Promise<IosSigningOptions | undefined> {
|
||||
const iosJob = job as Ios.Job;
|
||||
const buildCredentials = iosJob.secrets?.buildCredentials;
|
||||
@@ -295,5 +315,7 @@ export async function resolveIosSigningOptionsAsync({
|
||||
provisioningProfile,
|
||||
keychainPath: credentials.keychainPath,
|
||||
signingIdentity: credentials.applicationTargetProvisioningProfile.data.certificateCommonName,
|
||||
useAppEntitlements,
|
||||
entitlementsPath,
|
||||
};
|
||||
}
|
||||
@@ -141,6 +141,10 @@ export async function uploadCacheAsync({
|
||||
return;
|
||||
}
|
||||
const textResult = await asyncResult(response.text());
|
||||
const errorMessage = extractValidationErrorMessage(textResult.value);
|
||||
if (errorMessage) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
|
||||
}
|
||||
|
||||
@@ -231,6 +235,19 @@ export async function uploadPublicCacheAsync({
|
||||
logger.info(`Uploaded cache archive to ${archivePath} (${formatBytes(size)}).`);
|
||||
}
|
||||
|
||||
function extractValidationErrorMessage(body: string | undefined): string | undefined {
|
||||
if (!body) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
const { errors } = JSON.parse(body);
|
||||
if (errors?.[0]?.code === 'VALIDATION_ERROR') {
|
||||
return errors[0].message;
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function compressCacheAsync({
|
||||
paths,
|
||||
workingDirectory,
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { type bunyan } from '@expo/logger';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildRuntimePlatform,
|
||||
BuildStepEnv,
|
||||
type BuildStepEnv,
|
||||
BuildStepInput,
|
||||
BuildStepInputValueTypeName,
|
||||
} from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import { type CustomBuildContext } from '../../customBuildContext';
|
||||
import { Sentry } from '../../sentry';
|
||||
import {
|
||||
type DetachedProcessHandle,
|
||||
getDeviceRunSessionIdOrThrow,
|
||||
getNgrokAuthtokenOrThrow,
|
||||
getNgrokTunnelDomainOrThrow,
|
||||
@@ -23,6 +26,7 @@ import {
|
||||
waitForFileAsync,
|
||||
} from '../utils/remoteDeviceRunSession';
|
||||
|
||||
const AGENT_DEVICE_PACKAGE_NAME = 'agent-device';
|
||||
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
|
||||
const SRC_DIR = '/tmp/agent-device-src';
|
||||
const DAEMON_JSON_PATH = path.join(os.homedir(), '.agent-device', 'daemon.json');
|
||||
@@ -64,34 +68,12 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
await spawn('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
||||
}
|
||||
|
||||
logger.info(
|
||||
packageVersion
|
||||
? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
|
||||
: `Cloning agent-device (latest) into ${SRC_DIR}.`
|
||||
);
|
||||
await cloneAgentDeviceAsync({ packageVersion, env, logger });
|
||||
|
||||
logger.info('Installing agent-device dependencies.');
|
||||
await spawn('bun', ['install', '--production'], {
|
||||
cwd: SRC_DIR,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Launching agent-device daemon.');
|
||||
spawnDetached({
|
||||
command: 'bun',
|
||||
args: ['run', 'src/daemon.ts'],
|
||||
cwd: SRC_DIR,
|
||||
env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
|
||||
});
|
||||
const daemonProcess = await startAgentDeviceDaemonAsync({ packageVersion, env, logger });
|
||||
|
||||
logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
|
||||
const { port: daemonPort, token: daemonToken } = await waitForFileAsync({
|
||||
filePath: DAEMON_JSON_PATH,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'agent-device daemon credentials',
|
||||
parse: parseDaemonInfo,
|
||||
const { port: daemonPort, token: daemonToken } = await waitForDaemonInfoAsync({
|
||||
daemonProcess,
|
||||
});
|
||||
logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
|
||||
|
||||
@@ -137,6 +119,93 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
});
|
||||
}
|
||||
|
||||
async function startAgentDeviceDaemonAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
packageVersion: string | undefined;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<DetachedProcessHandle> {
|
||||
const packageSpec = createAgentDevicePackageSpec(packageVersion);
|
||||
try {
|
||||
logger.info(`Installing ${packageSpec} globally with Bun.`);
|
||||
await spawn('bun', ['add', '--global', packageSpec], {
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
const daemonPath = getGlobalAgentDeviceDaemonPath(env);
|
||||
if (!fs.existsSync(daemonPath)) {
|
||||
throw new SystemError(`Expected agent-device daemon entry at ${daemonPath}.`);
|
||||
}
|
||||
|
||||
logger.info(`Launching daemon from ${daemonPath}.`);
|
||||
return spawnDetached({
|
||||
command: 'node',
|
||||
args: [daemonPath],
|
||||
env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
const bunVersion = await getBunVersionForDiagnosticsAsync(env);
|
||||
Sentry.capture(
|
||||
'Failed to start agent-device daemon from global Bun package; falling back to git clone',
|
||||
error,
|
||||
{
|
||||
level: 'warning',
|
||||
tags: {
|
||||
phase: 'agent-device-daemon-start',
|
||||
fallback: 'git-clone',
|
||||
},
|
||||
extras: {
|
||||
packageSpec,
|
||||
packageVersion: packageVersion ?? 'latest',
|
||||
bunVersion,
|
||||
bunInstallConfigured: Boolean(env.BUN_INSTALL?.trim()),
|
||||
},
|
||||
}
|
||||
);
|
||||
logger.warn(
|
||||
`Failed to start daemon from global ${packageSpec}; falling back to git clone: ${error.message}`
|
||||
);
|
||||
return await startAgentDeviceDaemonFromGitAsync({ packageVersion, env, logger });
|
||||
}
|
||||
}
|
||||
|
||||
async function startAgentDeviceDaemonFromGitAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
packageVersion: string | undefined;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<DetachedProcessHandle> {
|
||||
logger.info(
|
||||
packageVersion
|
||||
? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
|
||||
: `Cloning agent-device (latest) into ${SRC_DIR}.`
|
||||
);
|
||||
await cloneAgentDeviceAsync({ packageVersion, env, logger });
|
||||
|
||||
logger.info('Installing agent-device dependencies.');
|
||||
await spawn('bun', ['install', '--production'], {
|
||||
cwd: SRC_DIR,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Launching daemon from cloned agent-device source.');
|
||||
return spawnDetached({
|
||||
command: 'bun',
|
||||
args: ['run', 'src/daemon.ts'],
|
||||
cwd: SRC_DIR,
|
||||
env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
|
||||
});
|
||||
}
|
||||
|
||||
async function cloneAgentDeviceAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
@@ -153,6 +222,63 @@ async function cloneAgentDeviceAsync({
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForDaemonInfoAsync({
|
||||
daemonProcess,
|
||||
}: {
|
||||
daemonProcess: DetachedProcessHandle;
|
||||
}): Promise<{ port: number; token: string }> {
|
||||
try {
|
||||
return await waitForFileAsync({
|
||||
filePath: DAEMON_JSON_PATH,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'agent-device daemon credentials',
|
||||
parse: parseDaemonInfo,
|
||||
});
|
||||
} catch (err) {
|
||||
const output = daemonProcess.getOutput();
|
||||
throw new SystemError(
|
||||
`${
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: `Timed out waiting for agent-device daemon credentials.`
|
||||
}${output ? `\nagent-device daemon output:\n${output}` : ''}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getBunVersionForDiagnosticsAsync(env: BuildStepEnv): Promise<string> {
|
||||
try {
|
||||
const result = await spawn('bun', ['--version'], { stdio: 'pipe', env, cwd: os.tmpdir() });
|
||||
return result.stdout.trim() || 'unknown';
|
||||
} catch {
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function createAgentDevicePackageSpec(packageVersion: string | undefined): string {
|
||||
const versionSpec = packageVersion ? packageVersion.replace(/^v(?=\d)/, '') : 'latest';
|
||||
return `${AGENT_DEVICE_PACKAGE_NAME}@${versionSpec}`;
|
||||
}
|
||||
|
||||
function getGlobalAgentDeviceDaemonPath(env: BuildStepEnv): string {
|
||||
return path.join(
|
||||
getBunInstallDirectory(env),
|
||||
'install',
|
||||
'global',
|
||||
'node_modules',
|
||||
AGENT_DEVICE_PACKAGE_NAME,
|
||||
'dist',
|
||||
'src',
|
||||
'internal',
|
||||
'daemon.js'
|
||||
);
|
||||
}
|
||||
|
||||
function getBunInstallDirectory(env: BuildStepEnv): string {
|
||||
const bunInstall = env.BUN_INSTALL?.trim();
|
||||
return bunInstall ? bunInstall : path.join(os.homedir(), '.bun');
|
||||
}
|
||||
|
||||
function parseDaemonInfo(raw: string): { port: number; token: string } {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
|
||||
@@ -3,8 +3,8 @@ import { BuildStepEnv } from '@expo/steps';
|
||||
import spawn, { SpawnResult } from '@expo/turtle-spawn';
|
||||
import path from 'path';
|
||||
|
||||
import { XcodeBuildLogger } from './xcpretty';
|
||||
import { COMMON_FASTLANE_ENV } from '../../../common/fastlane';
|
||||
import { XcodeBuildLogger } from '../../../common/xcpretty';
|
||||
|
||||
export async function runFastlaneGym({
|
||||
workingDir,
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { bunyan } from '@expo/logger';
|
||||
import spawnAsync, { SpawnPromise, SpawnResult } from '@expo/spawn-async';
|
||||
import { ExpoRunFormatter } from '@expo/xcpretty';
|
||||
import assert from 'assert';
|
||||
import fg from 'fast-glob';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const CHECK_FILE_INTERVAL_MS = 1000;
|
||||
|
||||
export class XcodeBuildLogger {
|
||||
private loggerError?: Error;
|
||||
private flushing: boolean = false;
|
||||
private logReaderPromise?: SpawnPromise<SpawnResult>;
|
||||
private logsPath?: string;
|
||||
|
||||
constructor(
|
||||
private readonly logger: bunyan,
|
||||
private readonly projectRoot: string
|
||||
) {}
|
||||
|
||||
public async watchLogFiles(logsDirectory: string): Promise<void> {
|
||||
while (!this.flushing) {
|
||||
const logsFilename = await this.getBuildLogFilename(logsDirectory);
|
||||
if (logsFilename) {
|
||||
this.logsPath = path.join(logsDirectory, logsFilename);
|
||||
void this.startBuildLogger(this.logsPath);
|
||||
return;
|
||||
}
|
||||
await new Promise(res => setTimeout(res, CHECK_FILE_INTERVAL_MS));
|
||||
}
|
||||
}
|
||||
|
||||
public async flush(): Promise<void> {
|
||||
this.flushing = true;
|
||||
if (this.loggerError) {
|
||||
throw this.loggerError;
|
||||
}
|
||||
if (this.logReaderPromise) {
|
||||
this.logReaderPromise.child.kill('SIGINT');
|
||||
try {
|
||||
await this.logReaderPromise;
|
||||
} catch {}
|
||||
}
|
||||
if (this.logsPath) {
|
||||
await this.findBundlerErrors(this.logsPath);
|
||||
}
|
||||
}
|
||||
private async getBuildLogFilename(logsDirectory: string): Promise<string | undefined> {
|
||||
const paths = await fg('*.log', { cwd: logsDirectory });
|
||||
return paths.length >= 1 ? paths[0] : undefined;
|
||||
}
|
||||
|
||||
private async startBuildLogger(logsPath: string): Promise<void> {
|
||||
try {
|
||||
const formatter = ExpoRunFormatter.create(this.projectRoot, {
|
||||
// TODO: Can provide xcode project name for better parsing
|
||||
isDebug: false,
|
||||
});
|
||||
this.logReaderPromise = spawnAsync('tail', ['-n', '+0', '-f', logsPath], { stdio: 'pipe' });
|
||||
assert(this.logReaderPromise.child.stdout, 'stdout is not available');
|
||||
this.logReaderPromise.child.stdout.on('data', (data: string) => {
|
||||
const lines = formatter.pipe(data.toString());
|
||||
for (const line of lines) {
|
||||
this.logger.info(line);
|
||||
}
|
||||
});
|
||||
await this.logReaderPromise;
|
||||
|
||||
this.logger.info(formatter.getBuildSummary());
|
||||
} catch (err: any) {
|
||||
if (!this.flushing) {
|
||||
this.loggerError = err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async findBundlerErrors(logsPath: string): Promise<void> {
|
||||
try {
|
||||
const logFile = await fs.readFile(logsPath, 'utf-8');
|
||||
const match = logFile.match(
|
||||
/Welcome to Metro!\s* Fast - Scalable - Integrated\s*([\s\S]*)Run CLI with --verbose flag for more details.\nCommand PhaseScriptExecution failed with a nonzero exit code/
|
||||
);
|
||||
if (match) {
|
||||
this.logger.info(match[1]);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error({ err }, 'Failed to read Xcode logs');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,7 +131,7 @@ PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
bundler (~> 1.17)
|
||||
bundler (>= 2.4)
|
||||
cocoapods (~> 1.12)
|
||||
expo-cocoapods-proxy!
|
||||
rake (~> 10.0)
|
||||
@@ -139,4 +139,4 @@ DEPENDENCIES
|
||||
rubocop (~> 1.48)
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
4.0.13
|
||||
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
||||
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
||||
spec.require_paths = ['lib']
|
||||
|
||||
spec.add_development_dependency 'bundler', '~> 1.17'
|
||||
spec.add_development_dependency 'bundler', '>= 2.4'
|
||||
spec.add_development_dependency 'rake', '~> 10.0'
|
||||
spec.add_development_dependency 'rspec', '~> 3.0'
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
describe('config', () => {
|
||||
const originalEnvironment = process.env.ENVIRONMENT;
|
||||
const originalWorkerRuntimeConfigBase64 = process.env.WORKER_RUNTIME_CONFIG_BASE64;
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv('ENVIRONMENT', originalEnvironment);
|
||||
restoreEnv('WORKER_RUNTIME_CONFIG_BASE64', originalWorkerRuntimeConfigBase64);
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('parses cache URL fallbacks from worker runtime config', () => {
|
||||
process.env.ENVIRONMENT = 'staging';
|
||||
process.env.WORKER_RUNTIME_CONFIG_BASE64 = Buffer.from(
|
||||
JSON.stringify({
|
||||
buildId: 'build-id',
|
||||
npmCacheUrl: 'https://npm-cache.example',
|
||||
nodeJsCacheUrl: 'https://node-cache.example',
|
||||
mavenCacheUrl: 'https://maven-cache.example',
|
||||
cocoapodsCacheUrl: 'https://cocoapods-cache.example',
|
||||
})
|
||||
).toString('base64');
|
||||
|
||||
jest.isolateModules(() => {
|
||||
const config = require('../config').default;
|
||||
|
||||
expect(config.npmCacheUrl).toBe('https://npm-cache.example');
|
||||
expect(config.nodeJsCacheUrl).toBe('https://node-cache.example');
|
||||
expect(config.mavenCacheUrl).toBe('https://maven-cache.example');
|
||||
expect(config.cocoapodsCacheUrl).toBe('https://cocoapods-cache.example');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RuntimeSettings } from '@expo/build-tools';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import os from 'os';
|
||||
|
||||
@@ -6,9 +7,44 @@ import { getBuildEnv, getGradleMemoryOptions } from '../env';
|
||||
|
||||
describe(getBuildEnv.name, () => {
|
||||
const originalSentryDsn = config.sentry.dsn;
|
||||
const originalPlatform = process.platform;
|
||||
const originalCacheUrls = {
|
||||
EAS_NPM_CACHE_URL: process.env.EAS_NPM_CACHE_URL,
|
||||
EAS_NODEJS_CACHE_URL: process.env.EAS_NODEJS_CACHE_URL,
|
||||
EAS_MAVEN_CACHE_URL: process.env.EAS_MAVEN_CACHE_URL,
|
||||
EAS_COCOAPODS_CACHE_URL: process.env.EAS_COCOAPODS_CACHE_URL,
|
||||
EAS_BUILD_NPM_CACHE_URL: process.env.EAS_BUILD_NPM_CACHE_URL,
|
||||
NPM_CACHE_URL: process.env.NPM_CACHE_URL,
|
||||
NVM_NODEJS_ORG_MIRROR: process.env.NVM_NODEJS_ORG_MIRROR,
|
||||
EAS_BUILD_MAVEN_CACHE_URL: process.env.EAS_BUILD_MAVEN_CACHE_URL,
|
||||
EAS_BUILD_COCOAPODS_CACHE_URL: process.env.EAS_BUILD_COCOAPODS_CACHE_URL,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.EAS_BUILD_NPM_CACHE_URL;
|
||||
delete process.env.NPM_CACHE_URL;
|
||||
delete process.env.NVM_NODEJS_ORG_MIRROR;
|
||||
delete process.env.EAS_BUILD_MAVEN_CACHE_URL;
|
||||
delete process.env.EAS_BUILD_COCOAPODS_CACHE_URL;
|
||||
jest.spyOn(RuntimeSettings, 'getNpmCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getNodeJsCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getMavenCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getCocoapodsCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'isUsingIosPrecompiledModulesEnabled').mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.sentry.dsn = originalSentryDsn;
|
||||
restoreEnv('EAS_NPM_CACHE_URL', originalCacheUrls.EAS_NPM_CACHE_URL);
|
||||
restoreEnv('EAS_NODEJS_CACHE_URL', originalCacheUrls.EAS_NODEJS_CACHE_URL);
|
||||
restoreEnv('EAS_MAVEN_CACHE_URL', originalCacheUrls.EAS_MAVEN_CACHE_URL);
|
||||
restoreEnv('EAS_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_COCOAPODS_CACHE_URL);
|
||||
restoreEnv('EAS_BUILD_NPM_CACHE_URL', originalCacheUrls.EAS_BUILD_NPM_CACHE_URL);
|
||||
restoreEnv('NPM_CACHE_URL', originalCacheUrls.NPM_CACHE_URL);
|
||||
restoreEnv('NVM_NODEJS_ORG_MIRROR', originalCacheUrls.NVM_NODEJS_ORG_MIRROR);
|
||||
restoreEnv('EAS_BUILD_MAVEN_CACHE_URL', originalCacheUrls.EAS_BUILD_MAVEN_CACHE_URL);
|
||||
restoreEnv('EAS_BUILD_COCOAPODS_CACHE_URL', originalCacheUrls.EAS_BUILD_COCOAPODS_CACHE_URL);
|
||||
mockProcessPlatform(originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
@@ -62,6 +98,180 @@ describe(getBuildEnv.name, () => {
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds precompiled modules env vars for iOS jobs when enabled', () => {
|
||||
jest.mocked(RuntimeSettings.isUsingIosPrecompiledModulesEnabled).mockReturnValue(true);
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.EAS_USE_PRECOMPILED_MODULES).toBe('1');
|
||||
});
|
||||
|
||||
it('does not add precompiled modules env vars for Android jobs when enabled', () => {
|
||||
jest.mocked(RuntimeSettings.isUsingIosPrecompiledModulesEnabled).mockReturnValue(true);
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.ANDROID,
|
||||
type: Workflow.MANAGED,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.EAS_USE_PRECOMPILED_MODULES).toBeUndefined();
|
||||
});
|
||||
|
||||
it('leaves job-provided precompiled modules env vars in override position', () => {
|
||||
jest.mocked(RuntimeSettings.isUsingIosPrecompiledModulesEnabled).mockReturnValue(true);
|
||||
|
||||
const job = {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
builderEnvironment: {
|
||||
env: {
|
||||
EAS_USE_PRECOMPILED_MODULES: '0',
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const baseEnv = getBuildEnv({
|
||||
job,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect({ ...baseEnv, ...job.builderEnvironment.env }.EAS_USE_PRECOMPILED_MODULES).toBe('0');
|
||||
});
|
||||
|
||||
it('does not expose disabled Linux cache URLs in build env', () => {
|
||||
mockProcessPlatform('linux');
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
process.env.EAS_NODEJS_CACHE_URL = 'https://node.example';
|
||||
process.env.EAS_MAVEN_CACHE_URL = 'https://maven.example';
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.ANDROID,
|
||||
type: Workflow.MANAGED,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.NVM_NODEJS_ORG_MIRROR).toBeUndefined();
|
||||
expect(env.EAS_BUILD_NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_BUILD_MAVEN_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_NODEJS_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_MAVEN_CACHE_URL).toBeUndefined();
|
||||
});
|
||||
|
||||
it('does not expose disabled Darwin cache URLs in build env', () => {
|
||||
mockProcessPlatform('darwin');
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
process.env.EAS_NODEJS_CACHE_URL = 'https://node.example';
|
||||
process.env.EAS_COCOAPODS_CACHE_URL = 'https://pods.example';
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.NVM_NODEJS_ORG_MIRROR).toBeUndefined();
|
||||
expect(env.EAS_BUILD_NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_BUILD_COCOAPODS_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_NODEJS_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_COCOAPODS_CACHE_URL).toBeUndefined();
|
||||
});
|
||||
|
||||
it('exposes enabled cache URLs inferred from private worker environment variables', () => {
|
||||
mockProcessPlatform('linux');
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
process.env.EAS_NODEJS_CACHE_URL = 'https://node.example';
|
||||
process.env.EAS_MAVEN_CACHE_URL = 'https://maven.example';
|
||||
jest.mocked(RuntimeSettings.getNpmCacheUrl).mockReturnValue('https://npm.example');
|
||||
jest.mocked(RuntimeSettings.getNodeJsCacheUrl).mockReturnValue('https://node.example');
|
||||
jest.mocked(RuntimeSettings.getMavenCacheUrl).mockReturnValue('https://maven.example');
|
||||
|
||||
const env = getBuildEnv({
|
||||
job: {
|
||||
platform: Platform.ANDROID,
|
||||
type: Workflow.MANAGED,
|
||||
builderEnvironment: {
|
||||
env: {},
|
||||
},
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
projectId: 'project-id',
|
||||
metadata: {
|
||||
buildProfile: 'production',
|
||||
gitCommitHash: 'abc123',
|
||||
username: 'expo-user',
|
||||
} as any,
|
||||
buildId: 'build-id',
|
||||
});
|
||||
|
||||
expect(env.NPM_CACHE_URL).toBe('https://npm.example');
|
||||
expect(env.EAS_BUILD_NPM_CACHE_URL).toBe('https://npm.example');
|
||||
expect(env.NVM_NODEJS_ORG_MIRROR).toBe('https://node.example');
|
||||
expect(env.EAS_BUILD_MAVEN_CACHE_URL).toBe('https://maven.example');
|
||||
expect(env.EAS_NPM_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_NODEJS_CACHE_URL).toBeUndefined();
|
||||
expect(env.EAS_MAVEN_CACHE_URL).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sizes Gradle memory options from total memory', () => {
|
||||
const totalMemory = jest.spyOn(os, 'totalmem');
|
||||
|
||||
@@ -84,3 +294,18 @@ describe(getBuildEnv.name, () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockProcessPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
}
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
// @ts-nocheck
|
||||
import { RuntimeSettings } from '@expo/build-tools';
|
||||
import { Android, Ios, Job } from '@expo/eas-build-job';
|
||||
import templateFile from '@expo/template-file';
|
||||
import spawn, { SpawnResult } from '@expo/turtle-spawn';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import { prepareRuntimeEnvironment } from '../runtimeEnvironment';
|
||||
import { mkdirp, pathExists } from 'fs-extra';
|
||||
|
||||
import config from '../config';
|
||||
import {
|
||||
prepareRuntimeEnvironment,
|
||||
prepareRuntimeEnvironmentConfigFiles,
|
||||
} from '../runtimeEnvironment';
|
||||
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('@expo/template-file');
|
||||
jest.mock('@expo/turtle-spawn');
|
||||
|
||||
const spawnResult: SpawnResult = {
|
||||
@@ -31,10 +39,75 @@ const ctx = {
|
||||
const builderConfig: Ios.BuilderEnvironment | Android.BuilderEnvironment = {};
|
||||
|
||||
describe('prepareRuntimeEnvironment', () => {
|
||||
const originalEnvironment = config.env;
|
||||
const originalPlatform = process.platform;
|
||||
const originalCacheUrls = {
|
||||
EAS_NPM_CACHE_URL: process.env.EAS_NPM_CACHE_URL,
|
||||
EAS_MAVEN_CACHE_URL: process.env.EAS_MAVEN_CACHE_URL,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(spawn).mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
config.env = originalEnvironment;
|
||||
restoreEnv('EAS_NPM_CACHE_URL', originalCacheUrls.EAS_NPM_CACHE_URL);
|
||||
restoreEnv('EAS_MAVEN_CACHE_URL', originalCacheUrls.EAS_MAVEN_CACHE_URL);
|
||||
mockProcessPlatform(originalPlatform);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe(prepareRuntimeEnvironmentConfigFiles.name, () => {
|
||||
beforeEach(() => {
|
||||
config.env = 'production';
|
||||
process.env.EAS_NPM_CACHE_URL = 'https://npm.example';
|
||||
process.env.EAS_MAVEN_CACHE_URL = 'https://maven.example';
|
||||
});
|
||||
|
||||
it('does not prepare disabled Linux cache config files', async () => {
|
||||
mockProcessPlatform('linux');
|
||||
jest.spyOn(RuntimeSettings, 'getNpmCacheUrl').mockReturnValue(null);
|
||||
jest.spyOn(RuntimeSettings, 'getMavenCacheUrl').mockReturnValue(null);
|
||||
|
||||
await prepareRuntimeEnvironmentConfigFiles();
|
||||
|
||||
expect(spawn).not.toHaveBeenCalledWith('npm', [
|
||||
'config',
|
||||
'set',
|
||||
'registry',
|
||||
'https://npm.example',
|
||||
]);
|
||||
expect(templateFile).not.toHaveBeenCalled();
|
||||
expect(mkdirp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('prepares enabled Linux cache config files', async () => {
|
||||
mockProcessPlatform('linux');
|
||||
jest.spyOn(RuntimeSettings, 'getNpmCacheUrl').mockReturnValue('https://npm.example');
|
||||
jest.spyOn(RuntimeSettings, 'getMavenCacheUrl').mockReturnValue('https://maven.example');
|
||||
|
||||
await prepareRuntimeEnvironmentConfigFiles();
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith('npm', [
|
||||
'config',
|
||||
'set',
|
||||
'registry',
|
||||
'https://npm.example',
|
||||
]);
|
||||
expect(templateFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('yarnrc.yml'),
|
||||
{ URL: 'https://npm.example' },
|
||||
expect.stringContaining('.yarnrc.yml')
|
||||
);
|
||||
expect(templateFile).toHaveBeenCalledWith(
|
||||
expect.stringContaining('init.gradle'),
|
||||
{ URL: 'https://maven.example' },
|
||||
expect.stringContaining('init.gradle')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('installNode', () => {
|
||||
describe('prepareRuntimeEnvironment', () => {
|
||||
beforeEach(() => {
|
||||
@@ -188,3 +261,18 @@ describe('prepareRuntimeEnvironment', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function mockProcessPlatform(platform: NodeJS.Platform): void {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
}
|
||||
|
||||
function restoreEnv(key: string, value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[key];
|
||||
} else {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { RuntimeSettings } from '@expo/build-tools';
|
||||
import { Env, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { spawnSync } from 'child_process';
|
||||
import micromatch from 'micromatch';
|
||||
@@ -35,9 +36,14 @@ export function getBuildEnv({
|
||||
setEnv(env, 'EAS_BUILD_PLATFORM', job.platform);
|
||||
setEnv(env, 'EAS_CLI_SENTRY_DSN', config.sentry.dsn);
|
||||
// NPM_CACHE_URL is deprecated
|
||||
setEnv(env, 'NPM_CACHE_URL', config.npmCacheUrl);
|
||||
setEnv(env, 'NVM_NODEJS_ORG_MIRROR', config.nodeJsCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_NPM_CACHE_URL', config.npmCacheUrl);
|
||||
const npmCacheUrl = RuntimeSettings.getNpmCacheUrl();
|
||||
const nodeJsCacheUrl = RuntimeSettings.getNodeJsCacheUrl();
|
||||
const mavenCacheUrl = RuntimeSettings.getMavenCacheUrl();
|
||||
const cocoapodsCacheUrl = RuntimeSettings.getCocoapodsCacheUrl();
|
||||
|
||||
setEnv(env, 'NPM_CACHE_URL', npmCacheUrl);
|
||||
setEnv(env, 'NVM_NODEJS_ORG_MIRROR', nodeJsCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_NPM_CACHE_URL', npmCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_PROFILE', metadata.buildProfile);
|
||||
setEnv(env, 'EAS_BUILD_GIT_COMMIT_HASH', metadata.gitCommitHash);
|
||||
setEnv(env, 'EAS_BUILD_ID', buildId);
|
||||
@@ -47,8 +53,11 @@ export function getBuildEnv({
|
||||
|
||||
const runnerPlatform = job.platform;
|
||||
if (runnerPlatform === Platform.IOS) {
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', config.cocoapodsCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', cocoapodsCacheUrl);
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
if (RuntimeSettings.isUsingIosPrecompiledModulesEnabled()) {
|
||||
setEnv(env, 'EAS_USE_PRECOMPILED_MODULES', '1');
|
||||
}
|
||||
|
||||
if (job.builderEnvironment?.env?.EAS_USE_CACHE === '1') {
|
||||
setEnv(env, 'USE_CCACHE', '1');
|
||||
@@ -69,7 +78,7 @@ export function getBuildEnv({
|
||||
setEnv(env, 'ANDROID_CCACHE', binPath);
|
||||
}
|
||||
}
|
||||
setEnv(env, 'EAS_BUILD_MAVEN_CACHE_URL', config.mavenCacheUrl);
|
||||
setEnv(env, 'EAS_BUILD_MAVEN_CACHE_URL', mavenCacheUrl);
|
||||
}
|
||||
|
||||
if (config.env !== Environment.TEST) {
|
||||
@@ -136,7 +145,14 @@ export function getBuildEnv({
|
||||
}
|
||||
|
||||
function getFilteredEnv(): Env {
|
||||
const envToFilter = [...getAccessedEnvs(), 'KUBERNETES_*'];
|
||||
const envToFilter = [
|
||||
...getAccessedEnvs(),
|
||||
'EAS_NPM_CACHE_URL',
|
||||
'EAS_NODEJS_CACHE_URL',
|
||||
'EAS_MAVEN_CACHE_URL',
|
||||
'EAS_COCOAPODS_CACHE_URL',
|
||||
'KUBERNETES_*',
|
||||
];
|
||||
const envToReturn = micromatch(
|
||||
Object.keys(process.env),
|
||||
envToFilter.map(env => `!${env}`)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { RuntimeSettings } from '@expo/build-tools';
|
||||
|
||||
import config from './config';
|
||||
import logger from './logger';
|
||||
import { startServer } from './metricsServer';
|
||||
@@ -7,6 +9,16 @@ import { prepareWorkingdir } from './workingdir';
|
||||
import startWsServer from './ws';
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await RuntimeSettings.loadAsync({
|
||||
environment: config.env,
|
||||
logger,
|
||||
cacheUrlFallbacks: {
|
||||
npm: config.npmCacheUrl,
|
||||
nodejs: config.nodeJsCacheUrl,
|
||||
maven: config.mavenCacheUrl,
|
||||
cocoapods: config.cocoapodsCacheUrl,
|
||||
},
|
||||
});
|
||||
await prepareRuntimeEnvironmentConfigFiles();
|
||||
await prepareWorkingdir();
|
||||
startWsServer();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BuildContext } from '@expo/build-tools';
|
||||
import { BuildContext, RuntimeSettings } from '@expo/build-tools';
|
||||
import { Job, errors } from '@expo/eas-build-job';
|
||||
import templateFile from '@expo/template-file';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
@@ -23,25 +23,28 @@ export async function prepareRuntimeEnvironmentConfigFiles(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.npmCacheUrl) {
|
||||
const npmCacheUrl = RuntimeSettings.getNpmCacheUrl();
|
||||
const mavenCacheUrl = RuntimeSettings.getMavenCacheUrl();
|
||||
|
||||
if (npmCacheUrl) {
|
||||
// create ~/.npmrc
|
||||
await spawn('npm', ['config', 'set', 'registry', config.npmCacheUrl]);
|
||||
await spawn('npm', ['config', 'set', 'registry', npmCacheUrl]);
|
||||
// create ~/.yarnrc.yml
|
||||
await templateFile(
|
||||
path.join(__dirname, '../src/templates/yarnrc.yml'),
|
||||
{
|
||||
URL: config.npmCacheUrl,
|
||||
URL: npmCacheUrl,
|
||||
},
|
||||
path.join(os.homedir(), '.yarnrc.yml')
|
||||
);
|
||||
}
|
||||
|
||||
if (config.mavenCacheUrl) {
|
||||
if (mavenCacheUrl) {
|
||||
await fs.mkdirp(path.join(os.homedir(), '.gradle'));
|
||||
await templateFile(
|
||||
path.join(__dirname, '../src/templates/init.gradle'),
|
||||
{
|
||||
URL: config.mavenCacheUrl,
|
||||
URL: mavenCacheUrl,
|
||||
},
|
||||
path.join(os.homedir(), '.gradle/init.gradle')
|
||||
);
|
||||
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -ne 1 || ("$1" != "staging" && "$1" != "production") ]]; then
|
||||
echo "Usage: scripts/edit-runtime-settings staging|production" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "curl is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for tool in gcloud jq; do
|
||||
if ! command -v "$tool" >/dev/null 2>&1; then
|
||||
echo "$tool is required. Run \`mise install\` from the repository root and try again." >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
env="$1"
|
||||
object_uri="gs://eas-workflows-${env}/runtime-settings.json"
|
||||
object_url="https://storage.googleapis.com/eas-workflows-${env}/runtime-settings.json"
|
||||
cache_control="no-cache, no-store, must-revalidate"
|
||||
editor="${EDITOR:-vi}"
|
||||
|
||||
tmpfile="$(mktemp)"
|
||||
minified_tmpfile="$(mktemp)"
|
||||
downloaded_tmpfile="$(mktemp)"
|
||||
trap 'rm -f "$tmpfile" "$minified_tmpfile" "$downloaded_tmpfile"' EXIT
|
||||
|
||||
echo "Remote URL: $object_url"
|
||||
curl -fsSL "$object_url" | jq . >"$tmpfile"
|
||||
echo "Edit and verify the pretty-formatted JSON file before saving."
|
||||
|
||||
while true; do
|
||||
"$editor" "$tmpfile"
|
||||
if jq empty "$tmpfile" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
|
||||
read -r -n 1 -s -p "JSON is invalid. Press any key to reopen editor." _
|
||||
echo >&2
|
||||
done
|
||||
|
||||
jq -c . "$tmpfile" >"$minified_tmpfile"
|
||||
|
||||
echo "Contents to upload:"
|
||||
cat "$minified_tmpfile"
|
||||
echo
|
||||
|
||||
if [[ "$env" == "production" ]]; then
|
||||
confirmation=""
|
||||
read -r -p 'Type "production" to upload production runtime settings: ' confirmation
|
||||
if [[ "$confirmation" != "production" ]]; then
|
||||
echo "Production upload canceled." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
gcloud storage cp \
|
||||
--cache-control="$cache_control" \
|
||||
--content-type=application/json \
|
||||
--predefined-acl=publicRead \
|
||||
"$minified_tmpfile" \
|
||||
"$object_uri"
|
||||
|
||||
curl -fsSL "$object_url" >"$downloaded_tmpfile"
|
||||
if ! cmp -s "$minified_tmpfile" "$downloaded_tmpfile"; then
|
||||
echo "Read-after-write check failed: uploaded file differs from remote object." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Confirmed remote contents:"
|
||||
cat "$downloaded_tmpfile"
|
||||
echo
|
||||
@@ -789,15 +789,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:7.10.4, @babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:~7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/code-frame@npm:7.10.4"
|
||||
dependencies:
|
||||
"@babel/highlight": "npm:^7.10.4"
|
||||
checksum: 10c0/69e0f52986a1f40231d891224f420436629b6678711b68c088e97b7bdba1607aeb5eb9cfb070275c433f0bf43c37c134845db80d1cdbf5ac88a69b0bdcce9402
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:7.23.5, @babel/code-frame@npm:^7.23.5":
|
||||
version: 7.23.5
|
||||
resolution: "@babel/code-frame@npm:7.23.5"
|
||||
@@ -808,6 +799,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:~7.10.4":
|
||||
version: 7.10.4
|
||||
resolution: "@babel/code-frame@npm:7.10.4"
|
||||
dependencies:
|
||||
"@babel/highlight": "npm:^7.10.4"
|
||||
checksum: 10c0/69e0f52986a1f40231d891224f420436629b6678711b68c088e97b7bdba1607aeb5eb9cfb070275c433f0bf43c37c134845db80d1cdbf5ac88a69b0bdcce9402
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/code-frame@npm:^7.12.13":
|
||||
version: 7.12.13
|
||||
resolution: "@babel/code-frame@npm:7.12.13"
|
||||
@@ -2868,7 +2868,7 @@ __metadata:
|
||||
"@expo/logger": "npm:20.0.0"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/plist": "npm:^0.2.0"
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/repack-app": "npm:~0.6.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:20.1.0"
|
||||
@@ -3419,15 +3419,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/repack-app@npm:~0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "@expo/repack-app@npm:0.4.1"
|
||||
"@expo/repack-app@npm:~0.6.1":
|
||||
version: 0.6.1
|
||||
resolution: "@expo/repack-app@npm:0.6.1"
|
||||
dependencies:
|
||||
commander: "npm:^14.0.2"
|
||||
picocolors: "npm:^1.1.1"
|
||||
bin:
|
||||
repack-app: bin/cli.js
|
||||
checksum: 10c0/4bc30a7ca95221f079026633802c32894ef25d55677212e86859194bf1a042598eaa5977510a277c3ceee6af6bd0d0a3aff3e21c4510c37b7dea584dd927ed0e
|
||||
checksum: 10c0/fdf3088b4f327393056631270bf90a67ba949e7994892a62abc985bd9478119ab2f1d5be35fa1efb6bd1d2780c49a3dbb87cb669d9c65b44e3d9a0b41fbf091f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3614,16 +3614,15 @@ __metadata:
|
||||
linkType: soft
|
||||
|
||||
"@expo/xcpretty@npm:^4.3.1":
|
||||
version: 4.3.2
|
||||
resolution: "@expo/xcpretty@npm:4.3.2"
|
||||
version: 4.4.4
|
||||
resolution: "@expo/xcpretty@npm:4.4.4"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.10.4"
|
||||
"@babel/code-frame": "npm:^7.20.0"
|
||||
chalk: "npm:^4.1.0"
|
||||
find-up: "npm:^5.0.0"
|
||||
js-yaml: "npm:^4.1.0"
|
||||
bin:
|
||||
excpretty: build/cli.js
|
||||
checksum: 10c0/e524817b2e42fb8c8914fca7e8f7c2f723f4f6d338a57b7ae97cd3e76da8108af63a22d4c7dc2e96a192a248a242f6e0f8056f0ca53bc4fb5cd2e5ae428e0891
|
||||
checksum: 10c0/cd555ad49438dee2cc3f2950ecbef3048f7169bbdadc8db169cfcddaad13668fee6377c010624ed4079dc439c46d6023d2551da403a2070deec000bc864e8dd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -12119,16 +12118,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-up@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "find-up@npm:5.0.0"
|
||||
dependencies:
|
||||
locate-path: "npm:^6.0.0"
|
||||
path-exists: "npm:^4.0.0"
|
||||
checksum: 10c0/062c5a83a9c02f53cdd6d175a37ecf8f87ea5bbff1fdfb828f04bfa021441bc7583e8ebc0872a4c1baab96221fb8a8a275a19809fb93fbc40bd69ec35634069a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"find-yarn-workspace-root@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "find-yarn-workspace-root@npm:2.0.0"
|
||||
@@ -15375,15 +15364,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"locate-path@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "locate-path@npm:6.0.0"
|
||||
dependencies:
|
||||
p-locate: "npm:^5.0.0"
|
||||
checksum: 10c0/d3972ab70dfe58ce620e64265f90162d247e87159b6126b01314dd67be43d50e96a50b517bce2d9452a79409c7614054c277b5232377de50416564a77ac7aad3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lodash.clonedeep@npm:^4.5.0":
|
||||
version: 4.5.0
|
||||
resolution: "lodash.clonedeep@npm:4.5.0"
|
||||
@@ -17349,7 +17329,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-limit@npm:3.1.0, p-limit@npm:^3.0.1, p-limit@npm:^3.0.2, p-limit@npm:^3.1.0":
|
||||
"p-limit@npm:3.1.0, p-limit@npm:^3.0.1, p-limit@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "p-limit@npm:3.1.0"
|
||||
dependencies:
|
||||
@@ -17394,15 +17374,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-locate@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "p-locate@npm:5.0.0"
|
||||
dependencies:
|
||||
p-limit: "npm:^3.0.2"
|
||||
checksum: 10c0/2290d627ab7903b8b70d11d384fee714b797f6040d9278932754a6860845c4d3190603a0772a663c8cb5a7b21d1b16acb3a6487ebcafa9773094edc3dfe6009a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-map-series@npm:2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "p-map-series@npm:2.1.0"
|
||||
|
||||
Reference in new issue
Block a user