mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27a2e2abc7 | ||
|
|
149f5d86a4 | ||
|
|
18c2e82385 | ||
|
|
43ffe46428 | ||
|
|
fb9b49cd5b | ||
|
|
17eb5f0687 | ||
|
|
6297ac19d9 | ||
|
|
6d86f91dab | ||
|
|
cedfabe18a | ||
|
|
20a21908d0 | ||
|
|
18b1dd8d9d | ||
|
|
fce2a77909 | ||
|
|
c562f08da9 | ||
|
|
5dee409567 | ||
|
|
ea2cd4f318 | ||
|
|
639a7cce61 | ||
|
|
b0db6d1dfa | ||
|
|
783033acb9 | ||
|
|
4366f517c1 | ||
|
|
1e9a0981a5 | ||
|
|
43953d6f16 | ||
|
|
7bb8c0e749 | ||
|
|
150176debc | ||
|
|
fa6c877a81 | ||
|
|
9ee8758aac | ||
|
|
d084e9ab9d | ||
|
|
ec0ac26fc3 | ||
|
|
51fb25c689 | ||
|
|
81cfeede9a | ||
|
|
4bff24b71c | ||
|
|
503b62b73d | ||
|
|
aaab3c98a1 | ||
|
|
b64f25b36d | ||
|
|
1e209b00a8 |
No files matched your search
@@ -8,10 +8,36 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- [eas-cli] Add `--refresh-ad-hoc-provisioning-profile` flag to refresh managed ad-hoc provisioning profiles from App Store Connect before gathering build credentials in non-interactive mode. ([#3716](https://github.com/expo/eas-cli/pull/3716) by [@sswrk](https://github.com/sswrk))
|
||||
- [eas-build-job] Add optional `refreshAdHocProvisioningProfile` field to iOS build jobs. ([#3717](https://github.com/expo/eas-cli/pull/3717) by [@sswrk](https://github.com/sswrk))
|
||||
- [build-tools] Pass `refreshAdHocProvisioningProfile` through `eas build:internal` for git-based integration builds. ([#3770](https://github.com/expo/eas-cli/pull/3770) by [@sswrk](https://github.com/sswrk))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [19.0.8](https://github.com/expo/eas-cli/releases/tag/v19.0.8) - 2026-05-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] `eas go` now pre-selects the SDK version from the current project's `app.json` or `app.config.js` when available. ([#3776](https://github.com/expo/eas-cli/pull/3776) by [@gwdp](https://github.com/gwdp))
|
||||
|
||||
## [19.0.7](https://github.com/expo/eas-cli/releases/tag/v19.0.7) - 2026-05-21
|
||||
|
||||
## [19.0.6](https://github.com/expo/eas-cli/releases/tag/v19.0.6) - 2026-05-21
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [build-tools] Update the minimum Expo version required for iOS precompiled modules to 55.0.26. ([#3771](https://github.com/expo/eas-cli/pull/3771) by [@chrfalch](https://github.com/chrfalch))
|
||||
|
||||
## [19.0.5](https://github.com/expo/eas-cli/releases/tag/v19.0.5) - 2026-05-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Improve Sentry diagnostics for managed iOS entitlements config fallback. ([#3763](https://github.com/expo/eas-cli/pull/3763) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
## [19.0.4](https://github.com/expo/eas-cli/releases/tag/v19.0.4) - 2026-05-20
|
||||
|
||||
## [19.0.1](https://github.com/expo/eas-cli/releases/tag/v19.0.1) - 2026-05-19
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "19.0.1",
|
||||
"version": "19.1.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/build-tools",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -38,14 +38,14 @@
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/downloader": "19.0.0",
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"@expo/env": "^0.4.0",
|
||||
"@expo/logger": "19.0.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/plist": "^0.2.0",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "19.0.0",
|
||||
"@expo/steps": "19.1.0",
|
||||
"@expo/template-file": "19.0.0",
|
||||
"@expo/turtle-spawn": "19.0.0",
|
||||
"@expo/xcpretty": "^4.3.1",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { vol } from 'memfs';
|
||||
import { createTestAndroidJob } from '../../__tests__/utils/job';
|
||||
import { createMockLogger } from '../../__tests__/utils/logger';
|
||||
import { BuildContext } from '../../context';
|
||||
import { Datadog } from '../../datadog';
|
||||
import { restoreCredentials } from '../../android/credentials';
|
||||
import androidBuilder from '../android';
|
||||
import { runBuilderWithHooksAsync } from '../common';
|
||||
@@ -67,8 +68,11 @@ jest.mock('../../utils/prepareBuildExecutable', () => ({
|
||||
prepareExecutableAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
const datadogDistributionMock = jest.spyOn(Datadog, 'distribution');
|
||||
|
||||
describe(androidBuilder, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vol.fromJSON(
|
||||
{
|
||||
'/workingdir/env/.gitkeep': '',
|
||||
@@ -138,7 +142,6 @@ describe(androidBuilder, () => {
|
||||
});
|
||||
|
||||
it('marks the configure Android version phase as warning for legacy eas-build.gradle', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const job: Android.Job = {
|
||||
...createTestAndroidJob(),
|
||||
secrets: {
|
||||
@@ -157,23 +160,23 @@ describe(androidBuilder, () => {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect(datadogDistributionMock).toHaveBeenCalledWith(
|
||||
'eas.build.phase_duration',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
|
||||
build_phase: BuildPhase.CONFIGURE_ANDROID_VERSION.toLowerCase(),
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('marks the prepare credentials phase as warning for legacy eas-build.gradle', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const ctx = new BuildContext(createTestAndroidJob(), {
|
||||
workingdir: '/workingdir',
|
||||
logBuffer: { getLogs: () => [], getPhaseLogs: () => [] },
|
||||
@@ -182,23 +185,23 @@ describe(androidBuilder, () => {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
|
||||
await androidBuilder(ctx);
|
||||
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect(datadogDistributionMock).toHaveBeenCalledWith(
|
||||
'eas.build.phase_duration',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
|
||||
build_phase: BuildPhase.PREPARE_CREDENTIALS.toLowerCase(),
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('injects credentials and version config when both are configured', async () => {
|
||||
const reportBuildPhaseStats = jest.fn();
|
||||
const job: Android.Job = {
|
||||
...createTestAndroidJob(),
|
||||
version: {
|
||||
@@ -214,7 +217,6 @@ describe(androidBuilder, () => {
|
||||
__API_SERVER_URL: 'http://api.expo.test',
|
||||
},
|
||||
uploadArtifact: jest.fn(),
|
||||
reportBuildPhaseStats,
|
||||
});
|
||||
vol.mkdirSync('/workingdir/build/android/app', { recursive: true });
|
||||
vol.writeFileSync('/workingdir/build/android/app/eas-build.gradle', '// Legacy content');
|
||||
@@ -234,15 +236,19 @@ describe(androidBuilder, () => {
|
||||
versionName: '1.2.3',
|
||||
}
|
||||
);
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect(datadogDistributionMock).toHaveBeenCalledWith(
|
||||
'eas.build.phase_duration',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.PREPARE_CREDENTIALS,
|
||||
build_phase: BuildPhase.PREPARE_CREDENTIALS.toLowerCase(),
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
expect(reportBuildPhaseStats).toHaveBeenCalledWith(
|
||||
expect(datadogDistributionMock).toHaveBeenCalledWith(
|
||||
'eas.build.phase_duration',
|
||||
expect.any(Number),
|
||||
expect.objectContaining({
|
||||
buildPhase: BuildPhase.CONFIGURE_ANDROID_VERSION,
|
||||
build_phase: BuildPhase.CONFIGURE_ANDROID_VERSION.toLowerCase(),
|
||||
result: BuildPhaseResult.WARNING,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
resolveGradleCommand,
|
||||
runGradleCommand,
|
||||
} from '../android/gradle';
|
||||
import { parseGradleProfile, formatGradleProfileReport } from '../android/gradleProfile';
|
||||
import { Sentry } from '../sentry';
|
||||
import { eagerBundleAsync, shouldUseEagerBundle } from '../common/eagerBundle';
|
||||
import { prebuildAsync } from '../common/prebuild';
|
||||
import { setupAsync } from '../common/setup';
|
||||
@@ -180,6 +182,20 @@ async function buildAsync(ctx: BuildContext<Android.Job>): Promise<void> {
|
||||
});
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.GRADLE_BUILD_PROFILE, async () => {
|
||||
try {
|
||||
const androidDir = path.join(ctx.getReactNativeProjectDirectory(), 'android');
|
||||
const profileTasks = await parseGradleProfile(androidDir);
|
||||
if (profileTasks.length > 0) {
|
||||
const report = formatGradleProfileReport(profileTasks);
|
||||
ctx.logger.info(report);
|
||||
}
|
||||
} catch (err: any) {
|
||||
Sentry.capture('Failed to parse Gradle build profile', err);
|
||||
ctx.markBuildPhaseSkipped();
|
||||
}
|
||||
});
|
||||
|
||||
await ctx.runBuildPhase(BuildPhase.PRE_UPLOAD_ARTIFACTS_HOOK, async () => {
|
||||
await runHookIfPresent(ctx, Hook.PRE_UPLOAD_ARTIFACTS);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BuildJob } from '@expo/eas-build-job';
|
||||
import { ArchiveSourceType, BuildJob, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
|
||||
import { resolveEnvFromBuildProfileAsync, runEasBuildInternalAsync } from '../easBuildInternal';
|
||||
@@ -15,7 +15,7 @@ jest.mock('../../utils/easCli', () => ({
|
||||
|
||||
describe('easBuildInternal', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(resolveEasCommandPrefixAndEnvAsync).mockResolvedValue({
|
||||
cmd: 'npx',
|
||||
args: ['-y', 'eas-cli@latest'],
|
||||
@@ -69,4 +69,131 @@ describe('easBuildInternal', () => {
|
||||
expect(resolveEasCommandPrefixAndEnvAsync).toHaveBeenCalledWith();
|
||||
expect(spawn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes --refresh-ad-hoc-provisioning-profile to build:internal for iOS jobs with refreshAdHocProvisioningProfile', async () => {
|
||||
const logger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
child: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const internalJob = {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
triggeredBy: 'EAS_CLI',
|
||||
projectArchive: { type: ArchiveSourceType.URL, url: 'https://example.com' },
|
||||
projectRootDirectory: '.',
|
||||
secrets: {
|
||||
buildCredentials: {
|
||||
testapp: {
|
||||
distributionCertificate: {
|
||||
dataBase64: 'YmluYXJ5Y29udGVudDE=',
|
||||
password: 'distCertPassword',
|
||||
},
|
||||
provisioningProfileBase64: 'MnRuZXRub2N5cmFuaWI=',
|
||||
},
|
||||
},
|
||||
},
|
||||
initiatingUserId: 'user-id',
|
||||
appId: 'app-id',
|
||||
};
|
||||
|
||||
jest.mocked(spawn).mockResolvedValue({
|
||||
stdout: Buffer.from(
|
||||
JSON.stringify({
|
||||
job: internalJob,
|
||||
metadata: {},
|
||||
})
|
||||
),
|
||||
stderr: Buffer.from(''),
|
||||
} as any);
|
||||
|
||||
const job = {
|
||||
platform: Platform.IOS,
|
||||
buildProfile: 'preview',
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
appId: 'app-id',
|
||||
initiatingUserId: 'user-id',
|
||||
secrets: { robotAccessToken: 'token' },
|
||||
} as unknown as BuildJob;
|
||||
|
||||
await runEasBuildInternalAsync({
|
||||
job,
|
||||
logger,
|
||||
env: {},
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect(spawn).toHaveBeenCalledWith(
|
||||
'npx',
|
||||
expect.arrayContaining([
|
||||
'build:internal',
|
||||
'--platform',
|
||||
Platform.IOS,
|
||||
'--profile',
|
||||
'preview',
|
||||
'--refresh-ad-hoc-provisioning-profile',
|
||||
]),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves refreshAdHocProvisioningProfile from the incoming job', async () => {
|
||||
const logger = {
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
child: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const internalJob = {
|
||||
platform: Platform.IOS,
|
||||
type: Workflow.GENERIC,
|
||||
triggeredBy: 'EAS_CLI',
|
||||
projectArchive: { type: ArchiveSourceType.URL, url: 'https://example.com' },
|
||||
projectRootDirectory: '.',
|
||||
secrets: {
|
||||
buildCredentials: {
|
||||
testapp: {
|
||||
distributionCertificate: {
|
||||
dataBase64: 'YmluYXJ5Y29udGVudDE=',
|
||||
password: 'distCertPassword',
|
||||
},
|
||||
provisioningProfileBase64: 'MnRuZXRub2N5cmFuaWI=',
|
||||
},
|
||||
},
|
||||
},
|
||||
initiatingUserId: 'user-id',
|
||||
appId: 'app-id',
|
||||
};
|
||||
|
||||
jest.mocked(spawn).mockResolvedValue({
|
||||
stdout: Buffer.from(
|
||||
JSON.stringify({
|
||||
job: internalJob,
|
||||
metadata: {},
|
||||
})
|
||||
),
|
||||
stderr: Buffer.from(''),
|
||||
} as any);
|
||||
|
||||
const job = {
|
||||
platform: Platform.IOS,
|
||||
buildProfile: 'preview',
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
appId: 'app-id',
|
||||
initiatingUserId: 'user-id',
|
||||
secrets: { robotAccessToken: 'token' },
|
||||
} as unknown as BuildJob;
|
||||
|
||||
const { newJob } = await runEasBuildInternalAsync({
|
||||
job,
|
||||
logger,
|
||||
env: {},
|
||||
cwd: '/tmp/project',
|
||||
});
|
||||
|
||||
expect((newJob as any).refreshAdHocProvisioningProfile).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,12 @@
|
||||
import { BuildJob, Env, Metadata, sanitizeBuildJob, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import {
|
||||
BuildJob,
|
||||
Env,
|
||||
Ios,
|
||||
Metadata,
|
||||
Platform,
|
||||
sanitizeBuildJob,
|
||||
sanitizeMetadata,
|
||||
} from '@expo/eas-build-job';
|
||||
import { PipeMode, bunyan } from '@expo/logger';
|
||||
import { BuildStepEnv } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
@@ -42,6 +50,11 @@ export async function runEasBuildInternalAsync<TJob extends BuildJob>({
|
||||
autoSubmitArgs.push('--auto-submit');
|
||||
}
|
||||
|
||||
const refreshAdHocProvisioningProfileArgs = [];
|
||||
if (job.platform === Platform.IOS && job.refreshAdHocProvisioningProfile === true) {
|
||||
refreshAdHocProvisioningProfileArgs.push('--refresh-ad-hoc-provisioning-profile');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await spawn(
|
||||
cmd,
|
||||
@@ -53,6 +66,7 @@ export async function runEasBuildInternalAsync<TJob extends BuildJob>({
|
||||
'--profile',
|
||||
buildProfile,
|
||||
...autoSubmitArgs,
|
||||
...refreshAdHocProvisioningProfileArgs,
|
||||
],
|
||||
{
|
||||
cwd,
|
||||
@@ -138,6 +152,9 @@ function validateEasBuildInternalResult<TJob extends BuildJob>({
|
||||
// We want to retain values that we have set on the job.
|
||||
appId: oldJob.appId,
|
||||
initiatingUserId: oldJob.initiatingUserId,
|
||||
...(oldJob.platform === Platform.IOS && oldJob.refreshAdHocProvisioningProfile === true
|
||||
? { refreshAdHocProvisioningProfile: true }
|
||||
: null),
|
||||
}) as TJob;
|
||||
assert(newJob.platform === oldJob.platform, 'eas-cli returned a job for a wrong platform');
|
||||
const newMetadata = sanitizeMetadata(value.metadata);
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ExpoConfig } from '@expo/config';
|
||||
import {
|
||||
BuildPhase,
|
||||
BuildPhaseResult,
|
||||
BuildPhaseStats,
|
||||
Env,
|
||||
EnvironmentSecretType,
|
||||
GenericArtifactType,
|
||||
@@ -65,7 +64,6 @@ export interface BuildContextOptions {
|
||||
err?: Error,
|
||||
options?: { tags?: Record<string, string>; extras?: Record<string, string> }
|
||||
) => void;
|
||||
reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
|
||||
skipNativeBuild?: boolean;
|
||||
metadata?: Metadata;
|
||||
expoApiV2BaseUrl?: string;
|
||||
@@ -98,7 +96,6 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
private buildPhaseSkipped = false;
|
||||
private buildPhaseHasWarnings = false;
|
||||
private _appConfig?: Promise<ExpoConfig>;
|
||||
private readonly reportBuildPhaseStats?: (stats: BuildPhaseStats) => void;
|
||||
public readonly graphqlClient: Client;
|
||||
|
||||
constructor(job: TJob, options: BuildContextOptions) {
|
||||
@@ -114,7 +111,6 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
this._metadata = options.metadata;
|
||||
this.skipNativeBuild = options.skipNativeBuild;
|
||||
this.expoApiV2BaseUrl = options.expoApiV2BaseUrl;
|
||||
this.reportBuildPhaseStats = options.reportBuildPhaseStats;
|
||||
|
||||
const environmentSecrets = this.getEnvironmentSecrets(job);
|
||||
this._env = {
|
||||
@@ -355,7 +351,6 @@ export class BuildContext<TJob extends Job = Job> {
|
||||
}
|
||||
await this.collectAndUpdateEnvVariablesAsync();
|
||||
|
||||
this.reportBuildPhaseStats?.({ buildPhase: this.buildPhase, result, durationMs });
|
||||
if (this.job.platform) {
|
||||
Datadog.distribution('eas.build.phase_duration', durationMs, {
|
||||
build_phase: this.buildPhase.toLowerCase(),
|
||||
|
||||
@@ -9,6 +9,7 @@ exports[`fastfile createFastfileForResigningBuild should create Fastfile with al
|
||||
"com.example.app" => "/path/to/profiles/main.mobileprovision",
|
||||
"com.example.app.widget" => "/path/to/profiles/widget.mobileprovision",
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/Users/expo/Library/Keychains/login.keychain"
|
||||
)
|
||||
end
|
||||
@@ -22,6 +23,7 @@ exports[`fastfile createFastfileForResigningBuild should handle empty provisioni
|
||||
signing_identity: "iPhone Distribution",
|
||||
provisioning_profile: {
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
@@ -39,6 +41,7 @@ exports[`fastfile createFastfileForResigningBuild should handle multiple provisi
|
||||
"com.example.app.extension" => "/profiles/extension.mobileprovision",
|
||||
"com.example.app.intents" => "/profiles/intents.mobileprovision",
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
@@ -53,6 +56,7 @@ exports[`fastfile createFastfileForResigningBuild should handle paths with speci
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/path/with spaces/profile (1).mobileprovision",
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/Users/expo/Library/Keychains/login keychain.keychain"
|
||||
)
|
||||
end
|
||||
@@ -67,6 +71,7 @@ exports[`fastfile createFastfileForResigningBuild should handle single provision
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/tmp/profile.mobileprovision",
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
@@ -81,6 +86,7 @@ exports[`fastfile createFastfileForResigningBuild should produce valid Ruby synt
|
||||
provisioning_profile: {
|
||||
"com.example.app" => "/path/to/profile.mobileprovision",
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "/tmp/keychain"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -126,14 +126,14 @@ describe(installPods, () => {
|
||||
});
|
||||
|
||||
it('does not add precompiled modules env vars below the minimum Expo version', async () => {
|
||||
const ctx = makeIosBuildContext({ expoVersion: '55.0.17' });
|
||||
const ctx = makeIosBuildContext({ expoVersion: '55.0.25' });
|
||||
|
||||
await installPods(ctx, {});
|
||||
|
||||
expect((spawn as jest.Mock).mock.calls[0][2].env.EXPO_USE_PRECOMPILED_MODULES).toBeUndefined();
|
||||
expect(ctx.logger.info).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/^Detected expo=55\.0\.17; not enabling precompiled modules use because precompiled modules require expo>=/
|
||||
/^Detected expo=55\.0\.25; not enabling precompiled modules use because precompiled modules require expo>=55\.0\.26\./
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import semver from 'semver';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
const MIN_PRECOMPILED_MODULES_EXPO_VERSION = '55.0.21';
|
||||
const MIN_PRECOMPILED_MODULES_EXPO_VERSION = '55.0.26';
|
||||
const PRECOMPILED_MODULES_BASE_URL =
|
||||
'https://storage.googleapis.com/eas-build-precompiled-modules/';
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import { createSaveCacheFunction } from './functions/saveCache';
|
||||
import { createSendSlackMessageFunction } from './functions/sendSlackMessage';
|
||||
import { createStartAgentDeviceRemoteSessionBuildFunction } from './functions/startAgentDeviceRemoteSession';
|
||||
import { createStartAndroidEmulatorBuildFunction } from './functions/startAndroidEmulator';
|
||||
import { createStartArgentRemoteSessionBuildFunction } from './functions/startArgentRemoteSession';
|
||||
import { createStartCuttlefishDeviceBuildFunction } from './functions/startCuttlefishDevice';
|
||||
import { createStartIosSimulatorBuildFunction } from './functions/startIosSimulator';
|
||||
import { createStartServeSimRemoteSessionBuildFunction } from './functions/startServeSimRemoteSession';
|
||||
@@ -81,6 +82,7 @@ export function getEasFunctions(ctx: CustomBuildContext): BuildFunction[] {
|
||||
runFastlaneFunction(),
|
||||
parseXcactivitylogFunction(),
|
||||
createStartAgentDeviceRemoteSessionBuildFunction(ctx),
|
||||
createStartArgentRemoteSessionBuildFunction(ctx),
|
||||
createStartAndroidEmulatorBuildFunction(),
|
||||
createStartCuttlefishDeviceBuildFunction(),
|
||||
createStartIosSimulatorBuildFunction(),
|
||||
|
||||
@@ -8,26 +8,24 @@ import {
|
||||
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 { sleepAsync } from '../../utils/retry';
|
||||
import {
|
||||
DetachedProcessHandle,
|
||||
ensureBrewPackageInstalledAsync,
|
||||
ensureCloudflaredInstalledAsync,
|
||||
getDeviceRunSessionIdOrThrow,
|
||||
spawnDetached,
|
||||
startServeSimWithTunnelAsync,
|
||||
uploadRemoteSessionConfigAsync,
|
||||
waitForFileAsync,
|
||||
waitForMatchInOutputAsync,
|
||||
} from '../utils/remoteDeviceRunSession';
|
||||
|
||||
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');
|
||||
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
||||
const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
|
||||
const STARTUP_TIMEOUT_MS = 60_000;
|
||||
|
||||
export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
@@ -92,12 +90,12 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
});
|
||||
|
||||
logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
|
||||
await waitForFileAsync({
|
||||
const { port: daemonPort, token: daemonToken } = await waitForFileAsync({
|
||||
filePath: DAEMON_JSON_PATH,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'agent-device daemon',
|
||||
description: 'agent-device daemon credentials',
|
||||
parse: parseDaemonInfo,
|
||||
});
|
||||
const { port: daemonPort, token: daemonToken } = readDaemonInfo(DAEMON_JSON_PATH);
|
||||
logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
|
||||
|
||||
logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort}.`);
|
||||
@@ -148,62 +146,6 @@ export function createStartAgentDeviceRemoteSessionBuildFunction(
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureCloudflaredInstalledAsync({
|
||||
runtimePlatform,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
runtimePlatform: BuildRuntimePlatform;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<string> {
|
||||
if (runtimePlatform === BuildRuntimePlatform.DARWIN) {
|
||||
await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
|
||||
return 'cloudflared';
|
||||
}
|
||||
if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
|
||||
return 'cloudflared';
|
||||
}
|
||||
const cloudflaredArch = cloudflaredLinuxArchForNodeArch(os.arch());
|
||||
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cloudflaredArch}`;
|
||||
logger.info(`Downloading cloudflared from ${downloadUrl} to ${CLOUDFLARED_LINUX_INSTALL_PATH}.`);
|
||||
await spawn('sudo', ['curl', '-fsSL', '-o', CLOUDFLARED_LINUX_INSTALL_PATH, downloadUrl], {
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
await spawn('sudo', ['chmod', '+x', CLOUDFLARED_LINUX_INSTALL_PATH], { env, logger });
|
||||
// Return the absolute install path so the tunnel command works even when
|
||||
// /usr/local/bin is not on the step's PATH.
|
||||
return CLOUDFLARED_LINUX_INSTALL_PATH;
|
||||
}
|
||||
|
||||
function cloudflaredLinuxArchForNodeArch(arch: string): 'amd64' | 'arm64' {
|
||||
if (arch === 'x64') {
|
||||
return 'amd64';
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
return 'arm64';
|
||||
}
|
||||
throw new SystemError(
|
||||
`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`
|
||||
);
|
||||
}
|
||||
|
||||
async function isCommandAvailableAsync({
|
||||
command,
|
||||
env,
|
||||
}: {
|
||||
command: string;
|
||||
env: BuildStepEnv;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
await spawn('bash', ['-c', `command -v ${command}`], { env, ignoreStdio: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function cloneAgentDeviceAsync({
|
||||
packageVersion,
|
||||
env,
|
||||
@@ -220,54 +162,7 @@ async function cloneAgentDeviceAsync({
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForMatchInOutputAsync({
|
||||
process,
|
||||
pattern,
|
||||
timeoutMs,
|
||||
description,
|
||||
}: {
|
||||
process: DetachedProcessHandle;
|
||||
pattern: RegExp;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
}): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const match = pattern.exec(process.getOutput());
|
||||
if (match) {
|
||||
return match[1] ?? match[0];
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new SystemError(
|
||||
`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForFileAsync({
|
||||
filePath,
|
||||
timeoutMs,
|
||||
description,
|
||||
}: {
|
||||
filePath: string;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
}): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
await fs.promises.access(filePath);
|
||||
return;
|
||||
} catch {
|
||||
// not yet; keep polling
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new SystemError(`Timed out waiting for ${description} to write ${filePath}.`);
|
||||
}
|
||||
|
||||
function readDaemonInfo(filePath: string): { port: number; token: string } {
|
||||
const raw = fs.readFileSync(filePath, 'utf8');
|
||||
function parseDaemonInfo(raw: string): { port: number; token: string } {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (
|
||||
!parsed ||
|
||||
@@ -276,7 +171,7 @@ function readDaemonInfo(filePath: string): { port: number; token: string } {
|
||||
typeof (parsed as { token: unknown }).token !== 'string'
|
||||
) {
|
||||
throw new SystemError(
|
||||
`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`
|
||||
'Expected daemon credentials to contain { "httpPort": <number>, "token": "..." }.'
|
||||
);
|
||||
}
|
||||
const { httpPort, token } = parsed as { httpPort: number; token: string };
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import {
|
||||
BuildFunction,
|
||||
BuildRuntimePlatform,
|
||||
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 { z } from 'zod';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import {
|
||||
ensureCloudflaredInstalledAsync,
|
||||
getDeviceRunSessionIdOrThrow,
|
||||
spawnDetached,
|
||||
startServeSimWithTunnelAsync,
|
||||
uploadRemoteSessionConfigAsync,
|
||||
waitForFileAsync,
|
||||
waitForMatchInOutputAsync,
|
||||
} from '../utils/remoteDeviceRunSession';
|
||||
|
||||
const ARGENT_PACKAGE_NAME = '@swmansion/argent';
|
||||
const ARGENT_STATE_FILE = path.join(os.homedir(), '.argent', 'tool-server.json');
|
||||
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
||||
const STARTUP_TIMEOUT_MS = 60_000;
|
||||
|
||||
const ArgentToolServerStateSchema = z.object({ port: z.number() });
|
||||
|
||||
export function createStartArgentRemoteSessionBuildFunction(
|
||||
ctx: CustomBuildContext
|
||||
): BuildFunction {
|
||||
return new BuildFunction({
|
||||
namespace: 'eas',
|
||||
id: 'start_argent_remote_session',
|
||||
name: 'Start argent remote session',
|
||||
__metricsId: 'eas/start_argent_remote_session',
|
||||
inputProviders: [
|
||||
BuildStepInput.createProvider({
|
||||
id: 'package_version',
|
||||
required: false,
|
||||
allowedValueTypeName: BuildStepInputValueTypeName.STRING,
|
||||
}),
|
||||
],
|
||||
fn: async ({ logger, global }, { inputs, env }) => {
|
||||
// Fail fast before any expensive setup if the orchestrator-injected
|
||||
// DEVICE_RUN_SESSION_ID env var is missing — without it we cannot
|
||||
// report the remote config back to the API server.
|
||||
const deviceRunSessionId = getDeviceRunSessionIdOrThrow(env);
|
||||
|
||||
const packageVersion = inputs.package_version.value as string | undefined;
|
||||
const versionSpec = packageVersion ?? 'latest';
|
||||
const { runtimePlatform } = global;
|
||||
logger.info(
|
||||
`Starting argent remote session (version: ${versionSpec}, runtime: ${runtimePlatform}).`
|
||||
);
|
||||
|
||||
if (runtimePlatform === BuildRuntimePlatform.DARWIN) {
|
||||
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
||||
await spawn('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
||||
}
|
||||
|
||||
logger.info('Ensuring cloudflared is installed.');
|
||||
const cloudflaredCommand = await ensureCloudflaredInstalledAsync({
|
||||
runtimePlatform,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
|
||||
// Stale state from a previous run would mask the new server's port.
|
||||
await fs.promises.rm(ARGENT_STATE_FILE, { force: true });
|
||||
|
||||
logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} via bunx.`);
|
||||
// `argent mcp` is the public entry that triggers @argent/tools-client
|
||||
// to spawn the tool-server detached + unref'd, so the tool-server
|
||||
// outlives this MCP process. ARGENT_IDLE_TIMEOUT_MINUTES=0 disables the
|
||||
// 30-min idle shutdown that would otherwise tear the tunnel down.
|
||||
spawnDetached({
|
||||
command: 'bunx',
|
||||
args: [`${ARGENT_PACKAGE_NAME}@${versionSpec}`, 'mcp'],
|
||||
env: { ...env, ARGENT_IDLE_TIMEOUT_MINUTES: '0' },
|
||||
});
|
||||
|
||||
logger.info(`Waiting for argent tool-server state at ${ARGENT_STATE_FILE}.`);
|
||||
const { port: toolServerPort } = await waitForFileAsync({
|
||||
filePath: ARGENT_STATE_FILE,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'argent tool-server state',
|
||||
parse: parseArgentToolServerState,
|
||||
});
|
||||
logger.info(`Argent tool-server is listening on port ${toolServerPort}.`);
|
||||
|
||||
logger.info(`Starting cloudflared tunnel to http://localhost:${toolServerPort}.`);
|
||||
const cloudflared = spawnDetached({
|
||||
command: cloudflaredCommand,
|
||||
args: ['tunnel', '--url', `http://localhost:${toolServerPort}`],
|
||||
env,
|
||||
});
|
||||
|
||||
logger.info('Waiting for a public tunnel URL.');
|
||||
const toolsUrl = await waitForMatchInOutputAsync({
|
||||
process: cloudflared,
|
||||
pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
description: 'cloudflared tunnel',
|
||||
});
|
||||
logger.info(`Tunnel is ready at ${toolsUrl}.`);
|
||||
|
||||
// serve-sim is iOS-only — Android sessions go without a preview URL.
|
||||
let webPreviewUrl: string | undefined;
|
||||
if (runtimePlatform === BuildRuntimePlatform.DARWIN) {
|
||||
const serveSim = await startServeSimWithTunnelAsync({
|
||||
env,
|
||||
logger,
|
||||
timeoutMs: STARTUP_TIMEOUT_MS,
|
||||
});
|
||||
webPreviewUrl = serveSim.previewUrl;
|
||||
logger.info(`Web preview URL: ${webPreviewUrl}`);
|
||||
}
|
||||
|
||||
await uploadRemoteSessionConfigAsync({
|
||||
ctx,
|
||||
deviceRunSessionId,
|
||||
remoteConfig: {
|
||||
toolsUrl,
|
||||
...(webPreviewUrl ? { webPreviewUrl } : {}),
|
||||
},
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
|
||||
// Keep the turtle job alive so the tool-server and tunnel stay reachable
|
||||
// until stopDeviceRunSession cancels the run.
|
||||
await new Promise<never>(() => {});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgentToolServerState(raw: string): { port: number } {
|
||||
const result = ArgentToolServerStateSchema.safeParse(JSON.parse(raw));
|
||||
if (!result.success) {
|
||||
throw new SystemError(
|
||||
`Expected tool-server state to contain { "port": <number>, ... }: ${result.error.message}`
|
||||
);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { createMockLogger } from '../../../../__tests__/utils/logger';
|
||||
import { Sentry } from '../../../../sentry';
|
||||
import {
|
||||
buildTargetMetrics,
|
||||
isCompileStep,
|
||||
collectTopLevelCompileSteps,
|
||||
formatReport,
|
||||
isCompileStep,
|
||||
parseAndReportXcactivitylog,
|
||||
} from '../xcactivitylog';
|
||||
|
||||
@@ -267,6 +267,9 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
'/tmp/workspace',
|
||||
'--reporter',
|
||||
'json',
|
||||
'--omit_warnings',
|
||||
'--omit_notes',
|
||||
'--trunc_large_issues',
|
||||
'--output',
|
||||
reportPath,
|
||||
],
|
||||
@@ -382,7 +385,7 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Build performance analysis failed during "parsing_xclogparser_output"',
|
||||
expect.any(Error),
|
||||
{ tags: { phase: 'parsing_xclogparser_output' } }
|
||||
expect.objectContaining({ tags: { phase: 'parsing_xclogparser_output' } })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -413,7 +416,7 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Build performance analysis failed during "parsing_xclogparser_output"',
|
||||
expect.any(Error),
|
||||
{ tags: { phase: 'parsing_xclogparser_output' } }
|
||||
expect.objectContaining({ tags: { phase: 'parsing_xclogparser_output' } })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -441,7 +444,45 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Build performance analysis failed during "running_xclogparser"',
|
||||
expect.objectContaining({ message: 'spawn xclogparser ENOENT' }),
|
||||
{ tags: { phase: 'running_xclogparser' } }
|
||||
expect.objectContaining({ tags: { phase: 'running_xclogparser' } })
|
||||
);
|
||||
});
|
||||
|
||||
it('surfaces spawn error stderr/stdout/status/signal as Sentry extras', async () => {
|
||||
const logger = createMockLogger();
|
||||
mockFilesystem();
|
||||
|
||||
const spawnError: any = new Error('xclogparser exited with non-zero code: 1');
|
||||
spawnError.stderr = 'XCLogParser fatal: missing xcactivitylog\n';
|
||||
spawnError.stdout = 'some stdout';
|
||||
spawnError.status = 1;
|
||||
spawnError.signal = null;
|
||||
|
||||
mockedDownloadFile.mockResolvedValue(undefined);
|
||||
mockedSpawn
|
||||
.mockRejectedValueOnce(new Error('xclogparser not found')) // version detect
|
||||
.mockResolvedValueOnce({ stdout: '', stderr: '' } as any) // unzip
|
||||
.mockRejectedValueOnce(spawnError); // xclogparser run
|
||||
|
||||
await parseAndReportXcactivitylog({
|
||||
derivedDataPath: '/tmp/derived-data',
|
||||
workspacePath: '/tmp/workspace',
|
||||
logger,
|
||||
env: TEST_ENV,
|
||||
});
|
||||
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Build performance analysis failed during "running_xclogparser"',
|
||||
expect.any(Error),
|
||||
{
|
||||
tags: { phase: 'running_xclogparser' },
|
||||
extras: {
|
||||
exitStatus: 1,
|
||||
signal: null,
|
||||
stderr: 'XCLogParser fatal: missing xcactivitylog\n',
|
||||
stdout: 'some stdout',
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -467,7 +508,7 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(
|
||||
'Build performance analysis failed during "downloading_xclogparser"',
|
||||
expect.objectContaining({ message: 'network unreachable' }),
|
||||
{ tags: { phase: 'downloading_xclogparser' } }
|
||||
expect.objectContaining({ tags: { phase: 'downloading_xclogparser' } })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -524,6 +565,9 @@ describe('parseAndReportXcactivitylog', () => {
|
||||
'/tmp/workspace',
|
||||
'--reporter',
|
||||
'json',
|
||||
'--omit_warnings',
|
||||
'--omit_notes',
|
||||
'--trunc_large_issues',
|
||||
'--output',
|
||||
reportPath,
|
||||
],
|
||||
|
||||
@@ -82,7 +82,15 @@ export async function parseAndReportXcactivitylog({
|
||||
} catch (err: any) {
|
||||
logger.info('Build performance analysis skipped.');
|
||||
const msg = `Build performance analysis failed during "${phase}"`;
|
||||
Sentry.capture(msg, err, { tags: { phase } });
|
||||
Sentry.capture(msg, err, {
|
||||
tags: { phase },
|
||||
extras: {
|
||||
exitStatus: err?.status,
|
||||
signal: err?.signal,
|
||||
stderr: err?.stderr?.slice(-4000),
|
||||
stdout: err?.stdout?.slice(-4000),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
if (tempDir) {
|
||||
await asyncResult(fs.rm(tempDir, { force: true, recursive: true }));
|
||||
@@ -203,6 +211,9 @@ async function runXclogparser({
|
||||
workspacePath,
|
||||
'--reporter',
|
||||
'json',
|
||||
'--omit_warnings',
|
||||
'--omit_notes',
|
||||
'--trunc_large_issues',
|
||||
'--output',
|
||||
outputPath,
|
||||
],
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { SystemError } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import { BuildStepEnv } from '@expo/steps';
|
||||
import { BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { graphql } from 'gql.tada';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
|
||||
import { CustomBuildContext } from '../../customBuildContext';
|
||||
import { sleepAsync } from '../../utils/retry';
|
||||
|
||||
const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
||||
const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
|
||||
|
||||
const START_DEVICE_RUN_SESSION_MUTATION = graphql(`
|
||||
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
||||
@@ -157,3 +160,112 @@ function matchLabeledUrl(content: string, label: string): string | null {
|
||||
const match = labelPattern.exec(content);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
export async function ensureCloudflaredInstalledAsync({
|
||||
runtimePlatform,
|
||||
env,
|
||||
logger,
|
||||
}: {
|
||||
runtimePlatform: BuildRuntimePlatform;
|
||||
env: BuildStepEnv;
|
||||
logger: bunyan;
|
||||
}): Promise<string> {
|
||||
if (runtimePlatform === BuildRuntimePlatform.DARWIN) {
|
||||
await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
|
||||
return 'cloudflared';
|
||||
}
|
||||
if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
|
||||
return 'cloudflared';
|
||||
}
|
||||
const cloudflaredArch = cloudflaredLinuxArchForNodeArch(os.arch());
|
||||
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cloudflaredArch}`;
|
||||
logger.info(`Downloading cloudflared from ${downloadUrl} to ${CLOUDFLARED_LINUX_INSTALL_PATH}.`);
|
||||
await spawn('sudo', ['curl', '-fsSL', '-o', CLOUDFLARED_LINUX_INSTALL_PATH, downloadUrl], {
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
await spawn('sudo', ['chmod', '+x', CLOUDFLARED_LINUX_INSTALL_PATH], { env, logger });
|
||||
// Return the absolute install path so the tunnel command works even when
|
||||
// /usr/local/bin is not on the step's PATH.
|
||||
return CLOUDFLARED_LINUX_INSTALL_PATH;
|
||||
}
|
||||
|
||||
function cloudflaredLinuxArchForNodeArch(arch: string): 'amd64' | 'arm64' {
|
||||
if (arch === 'x64') {
|
||||
return 'amd64';
|
||||
}
|
||||
if (arch === 'arm64') {
|
||||
return 'arm64';
|
||||
}
|
||||
throw new SystemError(
|
||||
`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`
|
||||
);
|
||||
}
|
||||
|
||||
async function isCommandAvailableAsync({
|
||||
command,
|
||||
env,
|
||||
}: {
|
||||
command: string;
|
||||
env: BuildStepEnv;
|
||||
}): Promise<boolean> {
|
||||
try {
|
||||
await spawn('bash', ['-c', `command -v ${command}`], { env, ignoreStdio: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForMatchInOutputAsync({
|
||||
process,
|
||||
pattern,
|
||||
timeoutMs,
|
||||
description,
|
||||
}: {
|
||||
process: DetachedProcessHandle;
|
||||
pattern: RegExp;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
}): Promise<string> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const match = pattern.exec(process.getOutput());
|
||||
if (match) {
|
||||
return match[1] ?? match[0];
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new SystemError(
|
||||
`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`
|
||||
);
|
||||
}
|
||||
|
||||
export async function waitForFileAsync<T>({
|
||||
filePath,
|
||||
timeoutMs,
|
||||
description,
|
||||
parse,
|
||||
}: {
|
||||
filePath: string;
|
||||
timeoutMs: number;
|
||||
description: string;
|
||||
parse: (raw: string) => T;
|
||||
}): Promise<T> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastError: unknown;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(filePath, 'utf8');
|
||||
return parse(raw);
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
}
|
||||
await sleepAsync(1_000);
|
||||
}
|
||||
throw new SystemError(
|
||||
`Timed out waiting for ${description} to be ready at ${filePath}${
|
||||
lastError instanceof Error ? `: ${lastError.message}` : ''
|
||||
}.`
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export const FastfileResignTemplate = `lane :do_resign do
|
||||
provisioning_profile: {<% _.forEach(PROFILES, function(profile) { %>
|
||||
"<%- profile.BUNDLE_ID %>" => "<%- profile.PATH %>",<% }); %>
|
||||
},
|
||||
use_app_entitlements: true,
|
||||
keychain_path: "<%- KEYCHAIN_PATH %>"
|
||||
)
|
||||
end
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { Sentry } from '../../sentry';
|
||||
import { findArtifacts } from '../artifacts';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../sentry', () => ({
|
||||
Sentry: {
|
||||
capture: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe(findArtifacts, () => {
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('with correct path', async () => {
|
||||
@@ -69,6 +76,44 @@ describe(findArtifacts, () => {
|
||||
});
|
||||
expect(loggerMock.error).toHaveBeenCalledTimes(0);
|
||||
expect(paths.length).toBe(2);
|
||||
expect(Sentry.capture).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('with absolute glob pattern', async () => {
|
||||
await fs.mkdirp('/tmp');
|
||||
await fs.mkdirp('/tmp/maestro_xctestrunner_xcodebuild_output123');
|
||||
await fs.mkdirp('/tmp/maestro_xctestrunner_xcodebuild_output456');
|
||||
const loggerMock = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
await expect(
|
||||
findArtifacts({
|
||||
rootDir: '/Users/expo/build',
|
||||
patternOrPath: '/tmp/maestro_xctestrunner_xcodebuild_output*',
|
||||
logger: loggerMock as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'There are no files matching pattern "/tmp/maestro_xctestrunner_xcodebuild_output*"'
|
||||
);
|
||||
expect(loggerMock.error).toHaveBeenCalledTimes(0);
|
||||
expect(Sentry.capture).toHaveBeenCalledWith(expect.any(Error), {
|
||||
tags: {
|
||||
source: 'find-artifacts',
|
||||
reason: 'absolute_path',
|
||||
},
|
||||
extras: {
|
||||
rootDir: '/Users/expo/build',
|
||||
patternOrPath: '/tmp/maestro_xctestrunner_xcodebuild_output*',
|
||||
currentCount: 0,
|
||||
dryRunCount: 2,
|
||||
currentSample: [],
|
||||
dryRunSample: [
|
||||
'/tmp/maestro_xctestrunner_xcodebuild_output123',
|
||||
'/tmp/maestro_xctestrunner_xcodebuild_output456',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('with missing file in empty directory', async () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import path from 'path';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import { BuildContext } from '../context';
|
||||
import { Sentry } from '../sentry';
|
||||
|
||||
export class FindArtifactsError extends Error {}
|
||||
|
||||
@@ -24,6 +25,11 @@ export async function findArtifacts({
|
||||
? [patternOrPath]
|
||||
: []
|
||||
: await fg(patternOrPath, { cwd: rootDir, onlyFiles: false });
|
||||
await maybeReportAbsoluteGlobDryRunMismatchAsync({
|
||||
rootDir,
|
||||
patternOrPath,
|
||||
files,
|
||||
});
|
||||
if (files.length === 0) {
|
||||
if (fg.isDynamicPattern(patternOrPath)) {
|
||||
throw new FindArtifactsError(`There are no files matching pattern "${patternOrPath}"`);
|
||||
@@ -48,6 +54,78 @@ export async function findArtifacts({
|
||||
});
|
||||
}
|
||||
|
||||
async function findArtifactsWithAbsoluteGlobSupportDryRunAsync(
|
||||
rootDir: string,
|
||||
patternOrPath: string
|
||||
): Promise<string[]> {
|
||||
return path.isAbsolute(patternOrPath) && !fg.isDynamicPattern(patternOrPath)
|
||||
? (await fs.pathExists(patternOrPath))
|
||||
? [patternOrPath]
|
||||
: []
|
||||
: await fg(patternOrPath, { cwd: rootDir, onlyFiles: false });
|
||||
}
|
||||
|
||||
async function maybeReportAbsoluteGlobDryRunMismatchAsync({
|
||||
rootDir,
|
||||
patternOrPath,
|
||||
files,
|
||||
}: {
|
||||
rootDir: string;
|
||||
patternOrPath: string;
|
||||
files: string[];
|
||||
}): Promise<void> {
|
||||
if (!path.isAbsolute(patternOrPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const filesWithAbsoluteGlobSupport = await findArtifactsWithAbsoluteGlobSupportDryRunAsync(
|
||||
rootDir,
|
||||
patternOrPath
|
||||
);
|
||||
if (areArtifactListsEqual(files, filesWithAbsoluteGlobSupport)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Sentry.capture(new Error('findArtifacts output changed for an absolute path'), {
|
||||
tags: {
|
||||
source: 'find-artifacts',
|
||||
reason: 'absolute_path',
|
||||
},
|
||||
extras: {
|
||||
rootDir,
|
||||
patternOrPath,
|
||||
currentCount: files.length,
|
||||
dryRunCount: filesWithAbsoluteGlobSupport.length,
|
||||
currentSample: files.slice(0, 20),
|
||||
dryRunSample: filesWithAbsoluteGlobSupport.slice(0, 20),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
Sentry.capture(err, {
|
||||
tags: {
|
||||
source: 'find-artifacts',
|
||||
reason: 'absolute_path_dry_run_failed',
|
||||
},
|
||||
extras: {
|
||||
rootDir,
|
||||
patternOrPath,
|
||||
currentCount: files.length,
|
||||
currentSample: files.slice(0, 20),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function areArtifactListsEqual(first: string[], second: string[]): boolean {
|
||||
if (first.length !== second.length) {
|
||||
return false;
|
||||
}
|
||||
const sortedFirst = [...first].sort();
|
||||
const sortedSecond = [...second].sort();
|
||||
return sortedFirst.every((artifactPath, index) => artifactPath === sortedSecond[index]);
|
||||
}
|
||||
|
||||
async function logMissingFileError(artifactPath: string, buildLogger: bunyan): Promise<void> {
|
||||
let currentPath = artifactPath;
|
||||
while (!(await fs.pathExists(currentPath))) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-eas-build-function",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"description": "Create functions for use in EAS Build custom builds.",
|
||||
"keywords": [
|
||||
"expo",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean": "rimraf ./build/ \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/steps": "19.0.0"
|
||||
"@expo/steps": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@expo/package-manager": "1.9.10",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-build-job",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "MIT",
|
||||
"author": "Expo <[email protected]>",
|
||||
|
||||
@@ -534,4 +534,47 @@ describe('Ios.JobSchema', () => {
|
||||
expect(value).toMatchObject(job);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('accepts optional refreshAdHocProvisioningProfile', () => {
|
||||
const job = {
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
type: Workflow.GENERIC,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'http://localhost:3000',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
};
|
||||
|
||||
const { value, error } = Ios.JobSchema.validate(job, joiOptions);
|
||||
expect(value.refreshAdHocProvisioningProfile).toBe(true);
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
|
||||
test('does not require refreshAdHocProvisioningProfile', () => {
|
||||
const job = {
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
type: Workflow.GENERIC,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: {
|
||||
type: ArchiveSourceType.URL,
|
||||
url: 'http://localhost:3000',
|
||||
},
|
||||
projectRootDirectory: '.',
|
||||
initiatingUserId: randomUUID(),
|
||||
appId: randomUUID(),
|
||||
};
|
||||
|
||||
const { value, error } = Ios.JobSchema.validate(job, joiOptions);
|
||||
expect(value.refreshAdHocProvisioningProfile).toBeUndefined();
|
||||
expect(error).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -181,12 +181,6 @@ export const CacheSchema = Joi.object({
|
||||
paths: Joi.array().items(Joi.string()).default([]),
|
||||
});
|
||||
|
||||
export interface BuildPhaseStats {
|
||||
buildPhase: BuildPhase;
|
||||
result: BuildPhaseResult;
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
const GitHubContextZ = z.object({
|
||||
triggering_actor: z.string().optional(),
|
||||
event_name: z.enum(['push', 'pull_request', 'workflow_dispatch', 'schedule']),
|
||||
|
||||
@@ -5,7 +5,6 @@ export {
|
||||
ArchiveSource,
|
||||
ArchiveSourceSchemaZ,
|
||||
BuildMode,
|
||||
BuildPhaseStats,
|
||||
BuildTrigger,
|
||||
EasCliNpmTags,
|
||||
Env,
|
||||
|
||||
@@ -144,6 +144,8 @@ export interface Job {
|
||||
appId: string;
|
||||
|
||||
environment?: string;
|
||||
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
}
|
||||
|
||||
const SecretsSchema = Joi.object({
|
||||
@@ -229,6 +231,8 @@ export const JobSchema = Joi.object({
|
||||
|
||||
environment: Joi.string(),
|
||||
|
||||
refreshAdHocProvisioningProfile: Joi.boolean(),
|
||||
|
||||
workflowInterpolationContext: Joi.object().custom(workflowInterpolationContext =>
|
||||
StaticWorkflowInterpolationContextZ.optional().parse(workflowInterpolationContext)
|
||||
),
|
||||
|
||||
+104
-101
@@ -190,7 +190,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -207,7 +207,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:usage [ACCOUNT_NAME]`
|
||||
|
||||
@@ -229,7 +229,7 @@ DESCRIPTION
|
||||
view account usage and billing for the current cycle
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/usage.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/account/usage.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -246,7 +246,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -260,7 +260,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -312,7 +312,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -333,7 +333,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -353,7 +353,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -373,7 +373,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -396,7 +396,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -406,7 +406,8 @@ start a build
|
||||
USAGE
|
||||
$ eas build [-p android|ios|all] [-e PROFILE_NAME] [--local] [--output <value>] [--wait] [--clear-cache]
|
||||
[-s | --auto-submit-with-profile PROFILE_NAME] [--what-to-test <value>] [-m <value>] [--build-logger-level
|
||||
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--verbose-logs] [--json] [--non-interactive]
|
||||
trace|debug|info|warn|error|fatal] [--freeze-credentials] [--refresh-ad-hoc-provisioning-profile] [--verbose-logs]
|
||||
[--json] [--non-interactive]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if
|
||||
@@ -425,6 +426,8 @@ FLAGS
|
||||
--local Run build locally [experimental]
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--output=<value> Output path for local build
|
||||
--refresh-ad-hoc-provisioning-profile Refresh managed ad-hoc provisioning profiles from App Store Connect
|
||||
before gathering build credentials
|
||||
--verbose-logs Use verbose logs for the build process
|
||||
--[no-]wait Wait for build(s) to complete
|
||||
--what-to-test=<value> Specify the "What to Test" information for the build in TestFlight
|
||||
@@ -434,7 +437,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -454,7 +457,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -472,7 +475,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -492,7 +495,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -517,7 +520,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -544,7 +547,7 @@ DESCRIPTION
|
||||
download a simulator/emulator build by build ID or fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -582,7 +585,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -622,7 +625,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -652,7 +655,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -680,7 +683,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -732,7 +735,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -750,7 +753,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -768,7 +771,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -785,7 +788,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -806,7 +809,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -827,7 +830,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -849,7 +852,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:insights`
|
||||
|
||||
@@ -873,7 +876,7 @@ DESCRIPTION
|
||||
display adoption, crash, and unique-user insights for a channel + runtime version
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/insights.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/insights.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -893,7 +896,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -915,7 +918,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -937,7 +940,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -972,7 +975,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -995,7 +998,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -1016,7 +1019,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -1033,7 +1036,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -1051,7 +1054,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1080,7 +1083,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1105,7 +1108,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1126,7 +1129,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1147,7 +1150,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1184,7 +1187,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1204,7 +1207,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1225,7 +1228,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1246,7 +1249,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1260,7 +1263,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1274,7 +1277,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1306,7 +1309,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1332,7 +1335,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1354,7 +1357,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1382,7 +1385,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1410,7 +1413,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1433,7 +1436,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1455,7 +1458,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1489,7 +1492,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1533,7 +1536,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1564,7 +1567,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1643,7 +1646,7 @@ DESCRIPTION
|
||||
connect a project to an App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/connect.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/asc/connect.ts)_
|
||||
|
||||
## `eas integrations:asc:disconnect`
|
||||
|
||||
@@ -1662,7 +1665,7 @@ DESCRIPTION
|
||||
disconnect the current project from its App Store Connect app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/disconnect.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/asc/disconnect.ts)_
|
||||
|
||||
## `eas integrations:asc:status`
|
||||
|
||||
@@ -1680,7 +1683,7 @@ DESCRIPTION
|
||||
show the App Store Connect app link status for the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/asc/status.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/asc/status.ts)_
|
||||
|
||||
## `eas integrations:convex:connect`
|
||||
|
||||
@@ -1702,7 +1705,7 @@ DESCRIPTION
|
||||
connect Convex to your Expo project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/connect.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/connect.ts)_
|
||||
|
||||
## `eas integrations:convex:dashboard`
|
||||
|
||||
@@ -1716,7 +1719,7 @@ DESCRIPTION
|
||||
open the Convex dashboard for the linked Convex project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/dashboard.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/dashboard.ts)_
|
||||
|
||||
## `eas integrations:convex:project`
|
||||
|
||||
@@ -1730,7 +1733,7 @@ DESCRIPTION
|
||||
display the Convex project linked to the current Expo app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/project.ts)_
|
||||
|
||||
## `eas integrations:convex:project:delete`
|
||||
|
||||
@@ -1748,7 +1751,7 @@ DESCRIPTION
|
||||
remove the Convex project link for the current Expo app from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/project/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/project/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team`
|
||||
|
||||
@@ -1762,7 +1765,7 @@ DESCRIPTION
|
||||
display Convex teams linked to the current Expo app's owner account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/team.ts)_
|
||||
|
||||
## `eas integrations:convex:team:delete [CONVEX_TEAM]`
|
||||
|
||||
@@ -1783,7 +1786,7 @@ DESCRIPTION
|
||||
remove a Convex team link from the current Expo app owner account's EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/team/delete.ts)_
|
||||
|
||||
## `eas integrations:convex:team:invite [CONVEX_TEAM]`
|
||||
|
||||
@@ -1803,7 +1806,7 @@ DESCRIPTION
|
||||
send a Convex team invitation to your verified email address
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/integrations/convex/team/invite.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/integrations/convex/team/invite.ts)_
|
||||
|
||||
## `eas login`
|
||||
|
||||
@@ -1855,7 +1858,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1873,7 +1876,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1891,7 +1894,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
@@ -1943,7 +1946,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1957,7 +1960,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1980,7 +1983,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
@@ -2004,7 +2007,7 @@ ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/project/new.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -2022,7 +2025,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -2055,7 +2058,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -2098,7 +2101,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -2119,7 +2122,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -2140,7 +2143,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -2165,7 +2168,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:insights GROUPID`
|
||||
|
||||
@@ -2192,7 +2195,7 @@ DESCRIPTION
|
||||
display launch, crash, unique-user, and size insights for an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/insights.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/insights.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -2219,7 +2222,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -2257,7 +2260,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -2285,7 +2288,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -2315,7 +2318,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -2336,7 +2339,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2360,7 +2363,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2381,7 +2384,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2403,7 +2406,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2423,7 +2426,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2442,7 +2445,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2465,7 +2468,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2482,7 +2485,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2603,7 +2606,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2623,7 +2626,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2647,7 +2650,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run FILE`
|
||||
|
||||
@@ -2687,7 +2690,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2709,7 +2712,7 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
@@ -2737,7 +2740,7 @@ FLAG DESCRIPTIONS
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2757,7 +2760,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2779,5 +2782,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v19.0.1/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v19.1.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+363
@@ -9913,6 +9913,83 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "deviceRunSessionsPaginated",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "after",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "filter",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "DeviceRunSessionFilterInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppDeviceRunSessionsConnection",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "environmentSecrets",
|
||||
"description": "Environment secrets for an app",
|
||||
@@ -12955,6 +13032,100 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppDeviceRunSessionEdge",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "DeviceRunSession",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppDeviceRunSessionsConnection",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppDeviceRunSessionEdge",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppFingerprintEdge",
|
||||
@@ -14382,6 +14553,39 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueActiveUserCount",
|
||||
"description": "Approximate count of unique users (`eas_client_id`) with at least one\nsupported-metric event in `[startTime, endTime)`. Uses ClickHouse's\nHyperLogLog `uniq()`, so the value can drift by a small percent on apps\nwith very large user bases. `metricNames` on the input is currently\nignored: the count is always over all supported metrics, matching the\nuniverse used by `appVersions` headline counts.",
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppObserveReleasesInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": null,
|
||||
@@ -14458,6 +14662,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeenUserCount",
|
||||
"description": "Unique users whose most recent supported-metric event in the queried\nrange ran this build's embedded bundle.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueUserCount",
|
||||
"description": null,
|
||||
@@ -14569,6 +14789,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeenUserCount",
|
||||
"description": "Unique users whose most recent supported-metric event in the queried\nrange was on this (appVersion, appBuildNumber) tuple.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueUserCount",
|
||||
"description": null,
|
||||
@@ -14584,6 +14820,30 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "updates",
|
||||
"description": "Updates seen on this build number in the queried time range. Each\nentry's counts and EAS builds are scoped to this (appVersion,\nappBuildNumber, appUpdateId) tuple.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "AppObserveAppUpdate",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -14755,6 +15015,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeenUserCount",
|
||||
"description": "Unique users whose most recent supported-metric event in the queried\nrange matched this update at its current nesting level.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueUserCount",
|
||||
"description": null,
|
||||
@@ -14854,6 +15130,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeenUserCount",
|
||||
"description": "Unique users whose most recent supported-metric event in\n`[startTime, endTime)` was on this version. Each active user\ncontributes to exactly one version, so summing this field across the\nreturned versions yields the total active users in the window (equal\nto `AppObserve.uniqueActiveUserCount` up to HyperLogLog noise).\nA version may be returned with `lastSeenUserCount = 0` when\nevery user who touched it later moved to a newer version. Users with\nno events in the window are not counted on any version.",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "metrics",
|
||||
"description": null,
|
||||
@@ -37371,6 +37663,77 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "DeviceRunSessionFilterInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "platforms",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "AppPlatform",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "statuses",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "DeviceRunSessionStatus",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "types",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "DeviceRunSessionType",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "DeviceRunSessionMutation",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "19.0.1",
|
||||
"version": "19.1.0",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -47,8 +47,8 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/eas-json": "19.0.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"@expo/eas-json": "19.1.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "19.0.0",
|
||||
@@ -63,7 +63,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "19.0.0",
|
||||
"@expo/steps": "19.1.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^4.8.3",
|
||||
"@oclif/plugin-autocomplete": "^3.2.40",
|
||||
@@ -221,9 +221,6 @@
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"simulator": {
|
||||
"description": "manage remote simulator sessions"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
import { getConfigFilePaths } from '@expo/config';
|
||||
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import Go from '../../commands/go';
|
||||
import { WorkflowRunStatus } from '../../graphql/generated';
|
||||
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log from '../../log';
|
||||
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { ensureActorHasPrimaryAccount } from '../../user/actions';
|
||||
import { detectProjectSdkVersionAsync } from '../../commands/go';
|
||||
import { mockTestCommand } from './utils';
|
||||
|
||||
jest.mock('@expo/config', () => ({
|
||||
...jest.requireActual('@expo/config'),
|
||||
getConfigFilePaths: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../project/expoConfig');
|
||||
jest.mock('../../log', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
log: jest.fn(),
|
||||
withTick: jest.fn(),
|
||||
newLine: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
markFreshLine: jest.fn(),
|
||||
error: jest.fn(),
|
||||
},
|
||||
learnMore: jest.fn().mockReturnValue(''),
|
||||
}));
|
||||
jest.mock('../../ora', () => ({
|
||||
ora: jest.fn().mockReturnValue({
|
||||
start: jest.fn().mockReturnThis(),
|
||||
stop: jest.fn().mockReturnThis(),
|
||||
fail: jest.fn().mockReturnThis(),
|
||||
succeed: jest.fn().mockReturnThis(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('fs-extra', () => ({
|
||||
ensureDir: jest.fn().mockResolvedValue(undefined),
|
||||
writeFile: jest.fn().mockResolvedValue(undefined),
|
||||
remove: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
jest.mock('../../user/actions');
|
||||
jest.mock('../../graphql/queries/WorkflowRunQuery');
|
||||
jest.mock('../../graphql/mutations/WorkflowRunMutation');
|
||||
jest.mock('../../project/uploadAccountScopedFileAsync');
|
||||
jest.mock('../../project/uploadAccountScopedProjectSourceAsync');
|
||||
jest.mock('../../build/utils/url');
|
||||
|
||||
const mockGetConfigFilePaths = jest.mocked(getConfigFilePaths);
|
||||
const mockGetPrivateExpoConfigAsync = jest.mocked(getPrivateExpoConfigAsync);
|
||||
|
||||
describe('detectProjectSdkVersionAsync', () => {
|
||||
it('returns undefined when no config file exists', async () => {
|
||||
mockGetConfigFilePaths.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null });
|
||||
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns the sdkVersion from the project config', async () => {
|
||||
mockGetConfigFilePaths.mockReturnValue({
|
||||
staticConfigPath: '/project/app.json',
|
||||
dynamicConfigPath: null,
|
||||
});
|
||||
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
|
||||
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBe('55.0.0');
|
||||
});
|
||||
|
||||
it('returns undefined when reading the config throws', async () => {
|
||||
mockGetConfigFilePaths.mockReturnValue({
|
||||
staticConfigPath: '/project/app.json',
|
||||
dynamicConfigPath: null,
|
||||
});
|
||||
mockGetPrivateExpoConfigAsync.mockRejectedValue(new Error('config error'));
|
||||
await expect(detectProjectSdkVersionAsync('/project')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
const mockAccount = { id: 'account-id', name: 'testuser' };
|
||||
const mockActor = {
|
||||
__typename: 'User' as const,
|
||||
id: 'user-id',
|
||||
username: 'testuser',
|
||||
primaryAccount: mockAccount,
|
||||
};
|
||||
|
||||
describe('Go command', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(ensureActorHasPrimaryAccount).mockReturnValue(mockAccount as any);
|
||||
jest.mocked(WorkflowRunQuery.expoGoRepackConfigurationAsync).mockResolvedValue({
|
||||
files: [],
|
||||
sdkVersion: '55.0.0',
|
||||
} as any);
|
||||
jest.mocked(WorkflowRunMutation.createExpoGoRepackWorkflowRunAsync).mockResolvedValue({
|
||||
id: 'run-id',
|
||||
} as any);
|
||||
jest.mocked(uploadAccountScopedProjectSourceAsync).mockResolvedValue({
|
||||
projectArchiveBucketKey: 'archive-key',
|
||||
});
|
||||
jest
|
||||
.mocked(uploadAccountScopedFileAsync)
|
||||
.mockResolvedValue({ fileBucketKey: 'file-key' } as any);
|
||||
jest.mocked(getWorkflowRunUrl).mockReturnValue('https://expo.dev/run/123');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function makeCmd(argv: string[] = []) {
|
||||
const ctx = {
|
||||
loggedIn: { actor: mockActor as any, graphqlClient: {} as any },
|
||||
analytics: {} as any,
|
||||
};
|
||||
const cmd = mockTestCommand(Go, ['--bundle-id', 'com.test.go', ...argv], ctx);
|
||||
jest.spyOn(cmd as any, 'ensureEasProjectAsync').mockResolvedValue('project-id');
|
||||
jest.spyOn(cmd as any, 'setupCredentialsAsync').mockResolvedValue({ id: 'asc-app-id' });
|
||||
jest.spyOn(cmd as any, 'monitorWorkflowJobsAsync').mockResolvedValue(WorkflowRunStatus.Success);
|
||||
return cmd;
|
||||
}
|
||||
|
||||
it('logs auto-selected SDK message and reports resolved version after dispatch', async () => {
|
||||
mockGetConfigFilePaths.mockReturnValue({
|
||||
staticConfigPath: '/app.json',
|
||||
dynamicConfigPath: null,
|
||||
});
|
||||
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
|
||||
|
||||
await makeCmd().run();
|
||||
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('SDK 55'));
|
||||
expect(Log.withTick).toHaveBeenCalledWith(expect.stringContaining('Using Expo Go SDK'));
|
||||
});
|
||||
|
||||
it('skips auto-select log when --sdk-version flag is provided', async () => {
|
||||
mockGetConfigFilePaths.mockReturnValue({
|
||||
staticConfigPath: '/app.json',
|
||||
dynamicConfigPath: null,
|
||||
});
|
||||
mockGetPrivateExpoConfigAsync.mockResolvedValue({ sdkVersion: '55.0.0' } as any);
|
||||
|
||||
await makeCmd(['--sdk-version', '55.0.0']).run();
|
||||
|
||||
expect(Log.log).not.toHaveBeenCalledWith(expect.stringContaining('Auto-selected'));
|
||||
});
|
||||
});
|
||||
@@ -42,6 +42,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel,
|
||||
freezeCredentials,
|
||||
refreshAdHocProvisioningProfile: refreshAdHocProvisioningProfileFlag,
|
||||
isVerboseLoggingEnabled,
|
||||
whatToTest,
|
||||
env,
|
||||
@@ -65,6 +66,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
isVerboseLoggingEnabled: boolean;
|
||||
whatToTest?: string;
|
||||
env: Record<string, string>;
|
||||
@@ -90,6 +92,8 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
|
||||
const requiredPackageManager = resolvePackageManager(projectDir);
|
||||
|
||||
const refreshAdHocProvisioningProfile = refreshAdHocProvisioningProfileFlag ?? false;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive,
|
||||
@@ -101,6 +105,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
easJsonCliConfig,
|
||||
vcsClient,
|
||||
freezeCredentials,
|
||||
refreshAdHocProvisioningProfile,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
|
||||
import { BuildContext } from '../../context';
|
||||
import { ensureIosCredentialsAsync } from '../credentials';
|
||||
|
||||
describe(ensureIosCredentialsAsync, () => {
|
||||
it('errors when refresh is enabled with local credentials source', async () => {
|
||||
const buildCtx = {
|
||||
buildProfile: {
|
||||
credentialsSource: CredentialsSource.LOCAL,
|
||||
simulator: false,
|
||||
withoutCredentials: false,
|
||||
},
|
||||
credentialsCtx: {
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
},
|
||||
} as BuildContext<Platform.IOS>;
|
||||
|
||||
await expect(ensureIosCredentialsAsync(buildCtx, [])).rejects.toThrow(
|
||||
'--refresh-ad-hoc-provisioning-profile cannot be used with credentialsSource "local". Use remote credentials or omit the flag.'
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reject refresh when credentials source is remote', async () => {
|
||||
const buildCtx = {
|
||||
buildProfile: {
|
||||
credentialsSource: CredentialsSource.REMOTE,
|
||||
simulator: true,
|
||||
withoutCredentials: false,
|
||||
distribution: 'internal',
|
||||
},
|
||||
credentialsCtx: {
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
},
|
||||
} as BuildContext<Platform.IOS>;
|
||||
|
||||
await expect(ensureIosCredentialsAsync(buildCtx, [])).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,16 @@ export async function ensureIosCredentialsAsync(
|
||||
return;
|
||||
}
|
||||
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
if (
|
||||
buildCtx.credentialsCtx.refreshAdHocProvisioningProfile &&
|
||||
credentialsSource === 'local'
|
||||
) {
|
||||
throw new Error(
|
||||
'--refresh-ad-hoc-provisioning-profile cannot be used with credentialsSource "local". Use remote credentials or omit the flag.'
|
||||
);
|
||||
}
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
@@ -24,8 +34,6 @@ export async function ensureIosCredentialsAsync(
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
|
||||
@@ -112,6 +112,9 @@ export async function prepareJobAsync(
|
||||
// See: https://github.com/expo/eas-build/pull/454
|
||||
appId: ctx.projectId,
|
||||
initiatingUserId: ctx.user.id,
|
||||
...(ctx.credentialsCtx.refreshAdHocProvisioningProfile && {
|
||||
refreshAdHocProvisioningProfile: true,
|
||||
}),
|
||||
};
|
||||
return sanitizeBuildJob(job) as Ios.Job;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export interface BuildFlags {
|
||||
message?: string;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
isVerboseLoggingEnabled?: boolean;
|
||||
whatToTest?: string;
|
||||
simulator?: SimulatorRunTarget;
|
||||
@@ -413,6 +414,7 @@ async function prepareAndStartBuildAsync({
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
|
||||
freezeCredentials: flags.freezeCredentials,
|
||||
refreshAdHocProvisioningProfile: flags.refreshAdHocProvisioningProfile,
|
||||
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
|
||||
whatToTest: flags.whatToTest,
|
||||
env,
|
||||
|
||||
@@ -35,4 +35,5 @@ export interface BuildFlags {
|
||||
message?: string;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
}
|
||||
@@ -40,6 +40,7 @@ interface RawBuildFlags {
|
||||
message?: string;
|
||||
'build-logger-level'?: LoggerLevel;
|
||||
'freeze-credentials': boolean;
|
||||
'refresh-ad-hoc-provisioning-profile': boolean;
|
||||
'verbose-logs'?: boolean;
|
||||
'what-to-test'?: string;
|
||||
}
|
||||
@@ -121,6 +122,11 @@ export default class Build extends EasCommand {
|
||||
default: false,
|
||||
description: 'Prevent the build from updating credentials in non-interactive mode',
|
||||
}),
|
||||
'refresh-ad-hoc-provisioning-profile': Flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'Refresh managed ad-hoc provisioning profiles from App Store Connect before gathering build credentials',
|
||||
}),
|
||||
'verbose-logs': Flags.boolean({
|
||||
default: false,
|
||||
description: 'Use verbose logs for the build process',
|
||||
@@ -188,6 +194,20 @@ export default class Build extends EasCommand {
|
||||
flags: RawBuildFlags
|
||||
): Omit<BuildFlags, 'requestedPlatform'> & { requestedPlatform?: RequestedPlatform } {
|
||||
const { json, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
if (flags['refresh-ad-hoc-provisioning-profile']) {
|
||||
if (!nonInteractive) {
|
||||
Errors.error(
|
||||
'--refresh-ad-hoc-provisioning-profile can only be used in non-interactive mode.',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
if (flags['freeze-credentials']) {
|
||||
Errors.error(
|
||||
'Cannot use --refresh-ad-hoc-provisioning-profile with --freeze-credentials.',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!flags.local && flags.output) {
|
||||
Errors.error('--output is allowed only for local builds', { exit: 1 });
|
||||
}
|
||||
@@ -249,6 +269,7 @@ export default class Build extends EasCommand {
|
||||
message,
|
||||
buildLoggerLevel: flags['build-logger-level'],
|
||||
freezeCredentials: flags['freeze-credentials'],
|
||||
refreshAdHocProvisioningProfile: flags['refresh-ad-hoc-provisioning-profile'],
|
||||
isVerboseLoggingEnabled: flags['verbose-logs'],
|
||||
whatToTest: flags['what-to-test'],
|
||||
};
|
||||
|
||||
@@ -40,6 +40,11 @@ export default class BuildInternal extends EasCommand {
|
||||
helpValue: 'PROFILE_NAME',
|
||||
exclusive: ['auto-submit'],
|
||||
}),
|
||||
'refresh-ad-hoc-provisioning-profile': Flags.boolean({
|
||||
default: false,
|
||||
description:
|
||||
'Refresh managed ad-hoc provisioning profiles from App Store Connect before gathering build credentials',
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -85,6 +90,7 @@ export default class BuildInternal extends EasCommand {
|
||||
profile: flags.profile,
|
||||
nonInteractive: true,
|
||||
freezeCredentials: false,
|
||||
refreshAdHocProvisioningProfile: flags['refresh-ad-hoc-provisioning-profile'],
|
||||
wait: false,
|
||||
clearCache: false,
|
||||
json: true,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { App, User, UserRole } from '@expo/apple-utils';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
@@ -28,6 +28,7 @@ import { WorkflowRunQuery } from '../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { ora } from '../ora';
|
||||
import { getPrivateExpoConfigAsync } from '../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { uploadAccountScopedFileAsync } from '../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../project/uploadAccountScopedProjectSourceAsync';
|
||||
@@ -45,6 +46,20 @@ function deriveBundleIdSlug(bundleId: string): string {
|
||||
return bundleId.split('.').filter(Boolean).pop()!;
|
||||
}
|
||||
|
||||
export async function detectProjectSdkVersionAsync(
|
||||
projectDir: string
|
||||
): Promise<string | undefined> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath && !paths.dynamicConfigPath) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
return (await getPrivateExpoConfigAsync(projectDir)).sdkVersion;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const TESTFLIGHT_GROUP_NAME = 'Team (Expo)';
|
||||
|
||||
async function setupTestFlightAsync(ascApp: App): Promise<void> {
|
||||
@@ -189,7 +204,13 @@ export default class Go extends EasCommand {
|
||||
});
|
||||
Log.withTick(`Logged in as ${chalk.cyan(getActorDisplayName(actor))}`);
|
||||
|
||||
const sdkVersion = flags['sdk-version'];
|
||||
const detectedSdkVersion = await detectProjectSdkVersionAsync(process.cwd());
|
||||
if (detectedSdkVersion && !flags['sdk-version']) {
|
||||
Log.log(
|
||||
`Current project using SDK ${detectedSdkVersion.split('.')[0]}. Auto-selected same version. To use a different version, pass --sdk-version.`
|
||||
);
|
||||
}
|
||||
const sdkVersion = flags['sdk-version'] ?? detectedSdkVersion;
|
||||
const bundleId = flags['bundle-id'] ?? this.generateBundleId(actor);
|
||||
if (!isBundleIdentifierValid(bundleId)) {
|
||||
throw new Error(
|
||||
@@ -231,7 +252,11 @@ export default class Go extends EasCommand {
|
||||
}
|
||||
);
|
||||
|
||||
const { workflowUrl, workflowRunId } = await this.dispatchWorkflowAsync(
|
||||
const {
|
||||
workflowUrl,
|
||||
workflowRunId,
|
||||
sdkVersion: resolvedSdkVersion,
|
||||
} = await this.dispatchWorkflowAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
actor,
|
||||
@@ -242,6 +267,7 @@ export default class Go extends EasCommand {
|
||||
tmpDir,
|
||||
vcsClient
|
||||
);
|
||||
Log.withTick(`Using Expo Go SDK ${chalk.cyan(resolvedSdkVersion.split('.')[0])}`);
|
||||
Log.withTick(`Build started: ${chalk.cyan(workflowUrl)}`);
|
||||
|
||||
const status = await this.monitorWorkflowJobsAsync(graphqlClient, workflowRunId);
|
||||
@@ -401,7 +427,7 @@ export default class Go extends EasCommand {
|
||||
sdkVersion: string | undefined,
|
||||
tmpDir: string,
|
||||
vcsClient: Client
|
||||
): Promise<{ workflowUrl: string; workflowRunId: string }> {
|
||||
): Promise<{ workflowUrl: string; workflowRunId: string; sdkVersion: string }> {
|
||||
const account = ensureActorHasPrimaryAccount(actor);
|
||||
|
||||
const repackConfig = await WorkflowRunQuery.expoGoRepackConfigurationAsync(graphqlClient, {
|
||||
@@ -451,7 +477,7 @@ export default class Go extends EasCommand {
|
||||
|
||||
const workflowUrl = getWorkflowRunUrl(account.name, deriveBundleIdSlug(bundleId), result.id);
|
||||
|
||||
return { workflowUrl, workflowRunId: result.id };
|
||||
return { workflowUrl, workflowRunId: result.id, sdkVersion: repackConfig.sdkVersion };
|
||||
}
|
||||
|
||||
private async monitorWorkflowJobsAsync(
|
||||
|
||||
@@ -171,6 +171,7 @@ describe(ObserveVersions, () => {
|
||||
firstSeenAt: '2025-06-01T00:00:00.000Z',
|
||||
eventCount: 100,
|
||||
uniqueUserCount: 50,
|
||||
lastSeenUserCount: 12,
|
||||
buildNumbers: [],
|
||||
updates: [],
|
||||
metrics: [],
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
DeviceRunSessionByIdQuery,
|
||||
DeviceRunSessionStatus,
|
||||
DeviceRunSessionType,
|
||||
JobRunStatus,
|
||||
} from '../../../graphql/generated';
|
||||
import { DeviceRunSessionQuery } from '../../../graphql/queries/DeviceRunSessionQuery';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
import SimulatorGet from '../get';
|
||||
|
||||
jest.mock('../../../graphql/queries/DeviceRunSessionQuery');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn(() => {
|
||||
const spinner = {
|
||||
fail: jest.fn(),
|
||||
start: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
};
|
||||
spinner.start.mockReturnValue(spinner);
|
||||
return spinner;
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../utils/json');
|
||||
|
||||
type DeviceRunSessionById = DeviceRunSessionByIdQuery['deviceRunSessions']['byId'];
|
||||
|
||||
const mockByIdAsync = jest.mocked(DeviceRunSessionQuery.byIdAsync);
|
||||
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
|
||||
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
|
||||
|
||||
function makeDeviceRunSession(overrides: Partial<DeviceRunSessionById> = {}): DeviceRunSessionById {
|
||||
return {
|
||||
id: 'session-123',
|
||||
status: DeviceRunSessionStatus.InProgress,
|
||||
type: DeviceRunSessionType.AgentDevice,
|
||||
app: {
|
||||
id: 'app-123',
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'account-123',
|
||||
name: 'testuser',
|
||||
},
|
||||
},
|
||||
remoteConfig: {
|
||||
__typename: 'AgentDeviceRunSessionRemoteConfig',
|
||||
agentDeviceRemoteSessionUrl: 'https://agent.example.com',
|
||||
agentDeviceRemoteSessionToken: 'token-123',
|
||||
webPreviewUrl: 'https://preview.example.com',
|
||||
},
|
||||
turtleJobRun: {
|
||||
id: 'job-123',
|
||||
status: JobRunStatus.InProgress,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function getMockOclifConfig(): Config {
|
||||
const config = new Config({ root: __dirname });
|
||||
config.runHook = async () => ({
|
||||
failures: [],
|
||||
successes: [],
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
describe(SimulatorGet, () => {
|
||||
const graphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): {
|
||||
command: SimulatorGet;
|
||||
getContextAsync: jest.SpyInstance;
|
||||
} {
|
||||
const command = new SimulatorGet(argv, mockConfig);
|
||||
// @ts-expect-error getContextAsync is protected
|
||||
const getContextAsync = jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
return { command, getContextAsync };
|
||||
}
|
||||
|
||||
it('emits JSON when --json is passed', async () => {
|
||||
const session = makeDeviceRunSession();
|
||||
mockByIdAsync.mockResolvedValue(session);
|
||||
|
||||
const { command, getContextAsync } = createCommand(['--id', 'session-123', '--json']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockEnableJsonOutput).toHaveBeenCalled();
|
||||
expect(getContextAsync).toHaveBeenCalledWith(SimulatorGet, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
expect(mockByIdAsync).toHaveBeenCalledWith(graphqlClient, 'session-123');
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({
|
||||
id: 'session-123',
|
||||
type: 'agent-device',
|
||||
status: DeviceRunSessionStatus.InProgress,
|
||||
jobRunUrl: 'https://expo.dev/accounts/testuser/projects/testapp/job-runs/job-123',
|
||||
remoteConfig: session.remoteConfig,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,213 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AppPlatform,
|
||||
DeviceRunSessionStatus,
|
||||
DeviceRunSessionType,
|
||||
DeviceRunSessionsByAppIdQuery,
|
||||
JobRunStatus,
|
||||
} from '../../../graphql/generated';
|
||||
import { DeviceRunSessionQuery } from '../../../graphql/queries/DeviceRunSessionQuery';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../../utils/json';
|
||||
import SimulatorList from '../list';
|
||||
|
||||
jest.mock('../../../graphql/queries/DeviceRunSessionQuery');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn(() => {
|
||||
const spinner = {
|
||||
fail: jest.fn(),
|
||||
start: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
};
|
||||
spinner.start.mockReturnValue(spinner);
|
||||
return spinner;
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../utils/json');
|
||||
|
||||
type DeviceRunSessionEdge =
|
||||
DeviceRunSessionsByAppIdQuery['app']['byId']['deviceRunSessionsPaginated']['edges'][number];
|
||||
type DeviceRunSessionNode = DeviceRunSessionEdge['node'];
|
||||
type DeviceRunSessionsPaginated =
|
||||
DeviceRunSessionsByAppIdQuery['app']['byId']['deviceRunSessionsPaginated'];
|
||||
|
||||
const mockListByAppIdAsync = jest.mocked(DeviceRunSessionQuery.listByAppIdAsync);
|
||||
const mockEnableJsonOutput = jest.mocked(enableJsonOutput);
|
||||
const mockPrintJsonOnlyOutput = jest.mocked(printJsonOnlyOutput);
|
||||
|
||||
function makeSession(overrides: Partial<DeviceRunSessionNode> = {}): DeviceRunSessionNode {
|
||||
return {
|
||||
id: 'session-123',
|
||||
status: DeviceRunSessionStatus.InProgress,
|
||||
type: DeviceRunSessionType.AgentDevice,
|
||||
platform: AppPlatform.Ios,
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
startedAt: '2025-01-01T00:00:05.000Z',
|
||||
finishedAt: null,
|
||||
app: {
|
||||
id: 'app-123',
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'account-123',
|
||||
name: 'testuser',
|
||||
},
|
||||
},
|
||||
turtleJobRun: {
|
||||
id: 'job-123',
|
||||
status: JobRunStatus.InProgress,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeConnection(
|
||||
nodes: DeviceRunSessionNode[],
|
||||
pageInfo: Partial<DeviceRunSessionsPaginated['pageInfo']> = {}
|
||||
): DeviceRunSessionsPaginated {
|
||||
return {
|
||||
edges: nodes.map((node, index) => ({ cursor: `cursor-${index}`, node })),
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: nodes.length > 0 ? 'cursor-0' : null,
|
||||
endCursor: nodes.length > 0 ? `cursor-${nodes.length - 1}` : null,
|
||||
...pageInfo,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getMockOclifConfig(): Config {
|
||||
const config = new Config({ root: __dirname });
|
||||
config.runHook = async () => ({
|
||||
failures: [],
|
||||
successes: [],
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
describe(SimulatorList, () => {
|
||||
const graphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockConfig = getMockOclifConfig();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
function createCommand(argv: string[]): {
|
||||
command: SimulatorList;
|
||||
getContextAsync: jest.SpyInstance;
|
||||
} {
|
||||
const command = new SimulatorList(argv, mockConfig);
|
||||
// @ts-expect-error getContextAsync is protected
|
||||
const getContextAsync = jest.spyOn(command, 'getContextAsync').mockResolvedValue({
|
||||
projectId: 'app-123',
|
||||
loggedIn: { graphqlClient },
|
||||
});
|
||||
return { command, getContextAsync };
|
||||
}
|
||||
|
||||
it('emits JSON when --json is passed', async () => {
|
||||
const session = makeSession();
|
||||
mockListByAppIdAsync.mockResolvedValue(makeConnection([session]));
|
||||
|
||||
const { command, getContextAsync } = createCommand(['--json']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockEnableJsonOutput).toHaveBeenCalled();
|
||||
expect(getContextAsync).toHaveBeenCalledWith(SimulatorList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
expect(mockListByAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: 'app-123',
|
||||
first: 10,
|
||||
after: undefined,
|
||||
filter: undefined,
|
||||
});
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({
|
||||
sessions: [
|
||||
{
|
||||
id: 'session-123',
|
||||
type: 'agent-device',
|
||||
status: DeviceRunSessionStatus.InProgress,
|
||||
platform: AppPlatform.Ios,
|
||||
createdAt: '2025-01-01T00:00:00.000Z',
|
||||
startedAt: '2025-01-01T00:00:05.000Z',
|
||||
finishedAt: undefined,
|
||||
jobRunUrl: 'https://expo.dev/accounts/testuser/projects/testapp/job-runs/job-123',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: 'cursor-0',
|
||||
endCursor: 'cursor-0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('passes filters and cursor to the query', async () => {
|
||||
mockListByAppIdAsync.mockResolvedValue(makeConnection([]));
|
||||
|
||||
const { command } = createCommand([
|
||||
'--json',
|
||||
'--status',
|
||||
'in-progress',
|
||||
'--status',
|
||||
'new',
|
||||
'--type',
|
||||
'argent',
|
||||
'--platform',
|
||||
'ios',
|
||||
'--limit',
|
||||
'25',
|
||||
'--after',
|
||||
'page-cursor',
|
||||
]);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockListByAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: 'app-123',
|
||||
first: 25,
|
||||
after: 'page-cursor',
|
||||
filter: {
|
||||
statuses: [DeviceRunSessionStatus.InProgress, DeviceRunSessionStatus.New],
|
||||
types: [DeviceRunSessionType.Argent],
|
||||
platforms: [AppPlatform.Ios],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('runs non-interactively without --json', async () => {
|
||||
const session = makeSession({ id: 'session-1', turtleJobRun: null });
|
||||
mockListByAppIdAsync.mockResolvedValue(makeConnection([session]));
|
||||
|
||||
const { command, getContextAsync } = createCommand(['--non-interactive']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockEnableJsonOutput).not.toHaveBeenCalled();
|
||||
expect(mockPrintJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
expect(getContextAsync).toHaveBeenCalledWith(SimulatorList, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
expect(mockListByAppIdAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles empty results', async () => {
|
||||
mockListByAppIdAsync.mockResolvedValue(makeConnection([]));
|
||||
|
||||
const { command } = createCommand(['--json']);
|
||||
await command.runAsync();
|
||||
|
||||
expect(mockPrintJsonOnlyOutput).toHaveBeenCalledWith({
|
||||
sessions: [],
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
startCursor: null,
|
||||
endCursor: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,14 +2,22 @@ import { Flags } from '@oclif/core';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { DeviceRunSessionStatus } from '../../graphql/generated';
|
||||
import { DeviceRunSessionQuery } from '../../graphql/queries/DeviceRunSessionQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { formatRemoteSessionInstructions } from '../../simulator/utils';
|
||||
import {
|
||||
deviceRunSessionTypeToFlagValue,
|
||||
formatRemoteSessionInstructions,
|
||||
} from '../../simulator/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class SimulatorGet extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] get info about a remote simulator session on EAS by its device run session ID';
|
||||
|
||||
@@ -18,7 +26,7 @@ export default class SimulatorGet extends EasCommand {
|
||||
description: 'Device run session ID',
|
||||
required: true,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -27,11 +35,16 @@ export default class SimulatorGet extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorGet);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorGet, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const fetchSpinner = ora(`Fetching device run session ${flags.id}`).start();
|
||||
@@ -48,6 +61,17 @@ export default class SimulatorGet extends EasCommand {
|
||||
? getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, session.turtleJobRun.id)
|
||||
: '';
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({
|
||||
id: session.id,
|
||||
type: deviceRunSessionTypeToFlagValue(session.type),
|
||||
status: session.status,
|
||||
jobRunUrl: jobRunUrl || undefined,
|
||||
remoteConfig: session.remoteConfig,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`ID: ${session.id}`);
|
||||
Log.log(`Type: ${session.type}`);
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getBareJobRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { getLimitFlagWithCustomValues } from '../../commandUtils/pagination';
|
||||
import {
|
||||
AppPlatform,
|
||||
DeviceRunSessionFilterInput,
|
||||
DeviceRunSessionStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { DeviceRunSessionQuery } from '../../graphql/queries/DeviceRunSessionQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE,
|
||||
DEVICE_RUN_SESSION_TYPE_FLAG_VALUES,
|
||||
deviceRunSessionTypeToFlagValue,
|
||||
} from '../../simulator/utils';
|
||||
import { fromNow } from '../../utils/date';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
const DEFAULT_LIMIT = 10;
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
const STATUS_FLAG_VALUES: Record<DeviceRunSessionStatus, string> = {
|
||||
[DeviceRunSessionStatus.New]: 'new',
|
||||
[DeviceRunSessionStatus.InProgress]: 'in-progress',
|
||||
[DeviceRunSessionStatus.Stopped]: 'stopped',
|
||||
[DeviceRunSessionStatus.Errored]: 'errored',
|
||||
};
|
||||
const STATUS_BY_FLAG_VALUE = Object.fromEntries(
|
||||
Object.entries(STATUS_FLAG_VALUES).map(([status, value]) => [
|
||||
value,
|
||||
status as DeviceRunSessionStatus,
|
||||
])
|
||||
);
|
||||
|
||||
const PLATFORM_FLAG_VALUES: Record<AppPlatform, string> = {
|
||||
[AppPlatform.Android]: 'android',
|
||||
[AppPlatform.Ios]: 'ios',
|
||||
};
|
||||
const PLATFORM_BY_FLAG_VALUE = Object.fromEntries(
|
||||
Object.entries(PLATFORM_FLAG_VALUES).map(([platform, value]) => [value, platform as AppPlatform])
|
||||
);
|
||||
|
||||
export default class SimulatorList extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] list remote simulator sessions for the current project';
|
||||
|
||||
static override flags = {
|
||||
status: Flags.option({
|
||||
description: 'Filter by session status (repeatable)',
|
||||
options: Object.values(STATUS_FLAG_VALUES),
|
||||
multiple: true,
|
||||
})(),
|
||||
type: Flags.option({
|
||||
description: 'Filter by session type (repeatable)',
|
||||
options: Object.values(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES),
|
||||
multiple: true,
|
||||
})(),
|
||||
platform: Flags.option({
|
||||
description: 'Filter by device platform (repeatable)',
|
||||
options: Object.values(PLATFORM_FLAG_VALUES),
|
||||
multiple: true,
|
||||
})(),
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: DEFAULT_LIMIT, limit: MAX_LIMIT }),
|
||||
after: Flags.string({
|
||||
description:
|
||||
'Cursor for pagination. Use the endCursor from a previous query to fetch the next page.',
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.ProjectId,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorList);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorList, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const filter: DeviceRunSessionFilterInput = {};
|
||||
if (flags.status && flags.status.length > 0) {
|
||||
filter.statuses = flags.status.map(value => STATUS_BY_FLAG_VALUE[value]);
|
||||
}
|
||||
if (flags.type && flags.type.length > 0) {
|
||||
filter.types = flags.type.map(value => DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE[value]);
|
||||
}
|
||||
if (flags.platform && flags.platform.length > 0) {
|
||||
filter.platforms = flags.platform.map(value => PLATFORM_BY_FLAG_VALUE[value]);
|
||||
}
|
||||
|
||||
const limit = flags.limit ?? DEFAULT_LIMIT;
|
||||
|
||||
const fetchSpinner = jsonFlag ? null : ora('Fetching device run sessions').start();
|
||||
let connection;
|
||||
try {
|
||||
connection = await DeviceRunSessionQuery.listByAppIdAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
first: limit,
|
||||
after: flags.after,
|
||||
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
||||
});
|
||||
fetchSpinner?.succeed(`Fetched ${connection.edges.length} device run session(s)`);
|
||||
} catch (err) {
|
||||
fetchSpinner?.fail('Failed to fetch device run sessions');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const sessions = connection.edges.map(edge => edge.node);
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({
|
||||
sessions: sessions.map(session => ({
|
||||
id: session.id,
|
||||
type: deviceRunSessionTypeToFlagValue(session.type),
|
||||
status: session.status,
|
||||
platform: session.platform,
|
||||
createdAt: session.createdAt,
|
||||
startedAt: session.startedAt ?? undefined,
|
||||
finishedAt: session.finishedAt ?? undefined,
|
||||
jobRunUrl: session.turtleJobRun
|
||||
? getBareJobRunUrl(
|
||||
session.app.ownerAccount.name,
|
||||
session.app.slug,
|
||||
session.turtleJobRun.id
|
||||
)
|
||||
: undefined,
|
||||
})),
|
||||
pageInfo: connection.pageInfo,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessions.length === 0) {
|
||||
Log.newLine();
|
||||
Log.log('No device run sessions found.');
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
const formattedEntries = sessions.map(session => {
|
||||
const jobRunUrl = session.turtleJobRun
|
||||
? getBareJobRunUrl(session.app.ownerAccount.name, session.app.slug, session.turtleJobRun.id)
|
||||
: null;
|
||||
const lines = [
|
||||
`ID: ${session.id}`,
|
||||
`Type: ${session.type}`,
|
||||
`Status: ${session.status}`,
|
||||
`Platform: ${session.platform}`,
|
||||
`Created: ${fromNow(new Date(session.createdAt))} ago`,
|
||||
];
|
||||
if (jobRunUrl) {
|
||||
lines.push(`URL: ${link(jobRunUrl)}`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
});
|
||||
Log.log(formattedEntries.join(`\n\n${chalk.dim('———')}\n\n`));
|
||||
|
||||
if (connection.pageInfo.hasNextPage && connection.pageInfo.endCursor) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`More results available. Re-run with --after ${connection.pageInfo.endCursor} to fetch the next page.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import { DeviceRunSessionQuery } from '../../graphql/queries/DeviceRunSessionQue
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE,
|
||||
DEVICE_RUN_SESSION_TYPE_FLAG_VALUES,
|
||||
DeviceRunSessionRemoteConfig,
|
||||
formatRemoteSessionInstructions,
|
||||
} from '../../simulator/utils';
|
||||
@@ -28,21 +30,8 @@ import nullthrows from 'nullthrows';
|
||||
const POLL_INTERVAL_MS = 5_000; // 5 seconds
|
||||
const POLL_TIMEOUT_MS = 15 * 60 * 1_000; // 15 minutes
|
||||
|
||||
// Mapping enum → CLI flag value. Declared as Record<DeviceRunSessionType, string>
|
||||
// so adding a new enum value in codegen fails the build until it is wired up here.
|
||||
const DEVICE_RUN_SESSION_TYPE_FLAG_VALUES: Record<DeviceRunSessionType, string> = {
|
||||
[DeviceRunSessionType.AgentDevice]: 'agent-device',
|
||||
[DeviceRunSessionType.Argent]: 'argent',
|
||||
[DeviceRunSessionType.ServeSim]: 'serve-sim',
|
||||
};
|
||||
|
||||
const DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE = Object.fromEntries(
|
||||
(Object.entries(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES) as [DeviceRunSessionType, string][]).map(
|
||||
([type, value]) => [value, type]
|
||||
)
|
||||
) as Record<string, DeviceRunSessionType>;
|
||||
|
||||
export default class SimulatorStart extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] start a remote simulator session on EAS and get instructions to connect to it';
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
EasNonInteractiveAndJsonFlags,
|
||||
resolveNonInteractiveAndJsonFlags,
|
||||
} from '../../commandUtils/flags';
|
||||
import { DeviceRunSessionMutation } from '../../graphql/mutations/DeviceRunSessionMutation';
|
||||
import { ora } from '../../ora';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class SimulatorStop extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description =
|
||||
'[EXPERIMENTAL] stop a remote simulator session on EAS by its device run session ID';
|
||||
|
||||
@@ -14,7 +19,7 @@ export default class SimulatorStop extends EasCommand {
|
||||
description: 'Device run session ID',
|
||||
required: true,
|
||||
}),
|
||||
...EASNonInteractiveFlag,
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -23,11 +28,16 @@ export default class SimulatorStop extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(SimulatorStop);
|
||||
const { json: jsonFlag, nonInteractive } = resolveNonInteractiveAndJsonFlags(flags);
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(SimulatorStop, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
const stopSpinner = ora(`🛑 Stopping device run session ${flags.id}`).start();
|
||||
@@ -37,6 +47,10 @@ export default class SimulatorStop extends EasCommand {
|
||||
flags.id
|
||||
);
|
||||
stopSpinner.succeed(`🎉 Device run session ${session.id} is ${session.status.toLowerCase()}`);
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput({ id: session.id, status: session.status });
|
||||
}
|
||||
} catch (err) {
|
||||
stopSpinner.fail(`Failed to stop device run session ${flags.id}`);
|
||||
throw err;
|
||||
|
||||
@@ -27,6 +27,7 @@ export class CredentialsContext {
|
||||
public readonly nonInteractive: boolean;
|
||||
public readonly autoAcceptCredentialReuse: boolean;
|
||||
public readonly freezeCredentials: boolean = false;
|
||||
public readonly refreshAdHocProvisioningProfile: boolean = false;
|
||||
public readonly projectDir: string;
|
||||
public readonly user: Actor;
|
||||
public readonly graphqlClient: ExpoGraphqlClient;
|
||||
@@ -52,6 +53,7 @@ export class CredentialsContext {
|
||||
vcsClient: Client;
|
||||
freezeCredentials?: boolean;
|
||||
autoAcceptCredentialReuse?: boolean;
|
||||
refreshAdHocProvisioningProfile?: boolean;
|
||||
env?: Env;
|
||||
}
|
||||
) {
|
||||
@@ -65,6 +67,7 @@ export class CredentialsContext {
|
||||
this.autoAcceptCredentialReuse = options.autoAcceptCredentialReuse ?? false;
|
||||
this.projectInfo = options.projectInfo;
|
||||
this.freezeCredentials = options.freezeCredentials ?? false;
|
||||
this.refreshAdHocProvisioningProfile = options.refreshAdHocProvisioningProfile ?? false;
|
||||
this.usesBroadcastPushNotifications =
|
||||
options.projectInfo?.exp.ios?.usesBroadcastPushNotifications ?? false;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,37 @@
|
||||
import { promptAsync } from '../.././../prompts';
|
||||
import { AppleDevice, AppleDeviceFragment } from '../../../graphql/generated';
|
||||
import { AppleDevice, AppleDeviceClass, AppleDeviceFragment } from '../../../graphql/generated';
|
||||
import { APPLE_DEVICE_CLASS_LABELS } from '../../../graphql/types/credentials/AppleDevice';
|
||||
import { ApplePlatform } from '../appstore/constants';
|
||||
|
||||
export function filterDevicesForApplePlatform(
|
||||
devices: AppleDeviceFragment[],
|
||||
applePlatform: ApplePlatform
|
||||
): AppleDeviceFragment[] {
|
||||
if (applePlatform === ApplePlatform.TV_OS) {
|
||||
throw new Error('Filtering for tvOS is not supported yet');
|
||||
}
|
||||
|
||||
if (applePlatform === ApplePlatform.VISION_OS) {
|
||||
throw new Error('Filtering for visionOS is not supported yet');
|
||||
}
|
||||
|
||||
return devices.filter(device => isDeviceCompatibleWithApplePlatform(device, applePlatform));
|
||||
}
|
||||
|
||||
function isDeviceCompatibleWithApplePlatform(
|
||||
device: AppleDeviceFragment,
|
||||
applePlatform: Exclude<ApplePlatform, ApplePlatform.TV_OS | ApplePlatform.VISION_OS>
|
||||
): boolean {
|
||||
switch (applePlatform) {
|
||||
case ApplePlatform.IOS:
|
||||
return (
|
||||
device.deviceClass === AppleDeviceClass.Iphone ||
|
||||
device.deviceClass === AppleDeviceClass.Ipad
|
||||
);
|
||||
case ApplePlatform.MAC_OS:
|
||||
return device.deviceClass === AppleDeviceClass.Mac;
|
||||
}
|
||||
}
|
||||
|
||||
export async function chooseDevicesAsync(
|
||||
allDevices: AppleDeviceFragment[],
|
||||
|
||||
@@ -6,7 +6,11 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import { resolveAppleTeamIfAuthenticatedAsync } from './AppleTeamUtils';
|
||||
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from './BuildCredentialsUtils';
|
||||
import { chooseDevicesAsync, formatDeviceLabel } from './DeviceUtils';
|
||||
import {
|
||||
chooseDevicesAsync,
|
||||
filterDevicesForApplePlatform,
|
||||
formatDeviceLabel,
|
||||
} from './DeviceUtils';
|
||||
import { SetUpDistributionCertificate } from './SetUpDistributionCertificate';
|
||||
import DeviceCreateAction, { RegistrationMethod } from '../../../devices/actions/create/action';
|
||||
import {
|
||||
@@ -18,6 +22,8 @@ import {
|
||||
IosAppBuildCredentialsFragment,
|
||||
IosDistributionType,
|
||||
} from '../../../graphql/generated';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
import Log from '../../../log';
|
||||
import { getApplePlatformFromTarget } from '../../../project/ios/target';
|
||||
import {
|
||||
@@ -29,9 +35,13 @@ import {
|
||||
import differenceBy from '../../../utils/expodash/differenceBy';
|
||||
import { CredentialsContext } from '../../context';
|
||||
import { MissingCredentialsNonInteractiveError } from '../../errors';
|
||||
import { getAscApiKeyForAppSubmissionsAsync } from '../api/GraphqlClient';
|
||||
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
|
||||
import { AppleTeamType, AuthenticationMode } from '../appstore/authenticateTypes';
|
||||
import { ProvisioningProfile } from '../appstore/Credentials.types';
|
||||
import { ApplePlatform } from '../appstore/constants';
|
||||
import { hasAscEnvVars } from '../appstore/resolveCredentials';
|
||||
import { MinimalAscApiKey } from '../credentials';
|
||||
import { Target } from '../types';
|
||||
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
|
||||
|
||||
@@ -51,11 +61,23 @@ export class SetUpAdhocProvisioningProfile {
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { app } = this.options;
|
||||
|
||||
if (ctx.refreshAdHocProvisioningProfile && ctx.freezeCredentials) {
|
||||
throw new Error(
|
||||
'Cannot refresh ad-hoc provisioning profile when credentials are frozen. Remove --freeze-credentials or --refresh-ad-hoc-provisioning-profile.'
|
||||
);
|
||||
}
|
||||
|
||||
const distCert = await new SetUpDistributionCertificate(
|
||||
app,
|
||||
IosDistributionType.AdHoc
|
||||
).runAsync(ctx);
|
||||
|
||||
if (ctx.nonInteractive && ctx.refreshAdHocProvisioningProfile) {
|
||||
await this.ensureAppStoreAuthenticatedForAdhocRefreshAsync(ctx, app);
|
||||
return await this.runWithDistributionCertificateAsync(ctx, distCert);
|
||||
}
|
||||
|
||||
const areBuildCredentialsSetup = await this.areBuildCredentialsSetupAsync(ctx);
|
||||
|
||||
if (ctx.nonInteractive) {
|
||||
@@ -111,6 +133,11 @@ export class SetUpAdhocProvisioningProfile {
|
||||
appleTeam
|
||||
);
|
||||
if (registeredAppleDevices.length === 0) {
|
||||
if (ctx.nonInteractive) {
|
||||
throw new Error(
|
||||
'No devices are registered for this Apple team. Register devices with eas device:create first.'
|
||||
);
|
||||
}
|
||||
const shouldRegisterDevices = await confirmAsync({
|
||||
message: `You don't have any registered devices yet. Would you like to register them now?`,
|
||||
initial: true,
|
||||
@@ -127,13 +154,13 @@ export class SetUpAdhocProvisioningProfile {
|
||||
const provisionedDeviceIdentifiers = (
|
||||
currentBuildCredentials?.provisioningProfile?.appleDevices ?? []
|
||||
).map(i => i.identifier);
|
||||
const chosenDevices = await chooseDevicesAsync(
|
||||
registeredAppleDevices,
|
||||
provisionedDeviceIdentifiers
|
||||
);
|
||||
const applePlatform = getApplePlatformFromTarget(target);
|
||||
const chosenDevices =
|
||||
ctx.nonInteractive && ctx.refreshAdHocProvisioningProfile
|
||||
? filterDevicesForApplePlatform(registeredAppleDevices, applePlatform)
|
||||
: await chooseDevicesAsync(registeredAppleDevices, provisionedDeviceIdentifiers);
|
||||
|
||||
// 4. Reuse or create the profile on Apple Developer Portal
|
||||
const applePlatform = getApplePlatformFromTarget(target);
|
||||
const profileType =
|
||||
applePlatform === ApplePlatform.TV_OS
|
||||
? ProfileType.TVOS_APP_ADHOC
|
||||
@@ -187,21 +214,25 @@ export class SetUpAdhocProvisioningProfile {
|
||||
Log.log(
|
||||
'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet'
|
||||
);
|
||||
const shouldContinue = await selectAsync(
|
||||
'Do you want to continue without provisioning these devices?',
|
||||
[
|
||||
{
|
||||
title: 'Yes',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
title: 'No (EAS CLI will exit)',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (!shouldContinue) {
|
||||
Errors.exit(1);
|
||||
if (ctx.nonInteractive && ctx.refreshAdHocProvisioningProfile) {
|
||||
// Continue without prompting in non-interactive refresh mode.
|
||||
} else {
|
||||
const shouldContinue = await selectAsync(
|
||||
'Do you want to continue without provisioning these devices?',
|
||||
[
|
||||
{
|
||||
title: 'Yes',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
title: 'No (EAS CLI will exit)',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
if (!shouldContinue) {
|
||||
Errors.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +402,67 @@ export class SetUpAdhocProvisioningProfile {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureAppStoreAuthenticatedForAdhocRefreshAsync(
|
||||
ctx: CredentialsContext,
|
||||
app: AppLookupParams
|
||||
): Promise<void> {
|
||||
if (hasAscEnvVars()) {
|
||||
await ctx.appStore.ensureAuthenticatedAsync({ mode: AuthenticationMode.API_KEY });
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedKey = await resolveAscApiKeyForAppCredentialsAsync({
|
||||
graphqlClient: ctx.graphqlClient,
|
||||
app,
|
||||
});
|
||||
if (!resolvedKey) {
|
||||
throw new Error(
|
||||
'No App Store Connect API Key found for ad-hoc provisioning profile refresh. In non-interactive mode, provide one via:\n' +
|
||||
' - Environment variables: EXPO_ASC_API_KEY_PATH, EXPO_ASC_KEY_ID, EXPO_ASC_ISSUER_ID\n' +
|
||||
' - EAS credentials service: configure an App Store Connect API Key for submissions on this app'
|
||||
);
|
||||
}
|
||||
|
||||
await ctx.appStore.ensureAuthenticatedAsync({
|
||||
mode: AuthenticationMode.API_KEY,
|
||||
ascApiKey: resolvedKey.ascApiKey,
|
||||
teamId: resolvedKey.teamId,
|
||||
teamName: resolvedKey.teamName,
|
||||
// Provide a non-enterprise team type to avoid interactive team-type resolution.
|
||||
// Ad-hoc profile handling below uses explicit ProfileType and does not branch on team.inHouse.
|
||||
teamType: AppleTeamType.COMPANY_OR_ORGANIZATION,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveAscApiKeyForAppCredentialsAsync({
|
||||
graphqlClient,
|
||||
app,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
app: AppLookupParams;
|
||||
}): Promise<{
|
||||
ascApiKey: MinimalAscApiKey;
|
||||
teamId?: string;
|
||||
teamName?: string;
|
||||
} | null> {
|
||||
const ascKeyFragment = await getAscApiKeyForAppSubmissionsAsync(graphqlClient, app);
|
||||
if (!ascKeyFragment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.log('Using App Store Connect API Key from EAS credentials service.');
|
||||
const fullKey = await AppStoreConnectApiKeyQuery.getByIdAsync(graphqlClient, ascKeyFragment.id);
|
||||
return {
|
||||
ascApiKey: {
|
||||
keyP8: fullKey.keyP8,
|
||||
keyId: fullKey.keyIdentifier,
|
||||
issuerId: fullKey.issuerIdentifier,
|
||||
},
|
||||
teamId: ascKeyFragment.appleTeam?.appleTeamIdentifier,
|
||||
teamName: ascKeyFragment.appleTeam?.appleTeamName ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function doUDIDsMatch(udidsA: string[], udidsB: string[]): boolean {
|
||||
|
||||
@@ -54,6 +54,18 @@ export class SetUpTargetBuildCredentials {
|
||||
ctx: CredentialsContext
|
||||
): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { app, distribution, enterpriseProvisioning, target } = this.options;
|
||||
if (ctx.refreshAdHocProvisioningProfile) {
|
||||
if (distribution !== 'internal') {
|
||||
throw new Error(
|
||||
'--refresh-ad-hoc-provisioning-profile is only supported for internal distribution builds.'
|
||||
);
|
||||
}
|
||||
if (enterpriseProvisioning === 'universal') {
|
||||
throw new Error(
|
||||
'--refresh-ad-hoc-provisioning-profile is only supported for ad-hoc internal builds.'
|
||||
);
|
||||
}
|
||||
}
|
||||
if (distribution === 'internal') {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return await new SetUpAdhocProvisioningProfile({ app, target }).runAsync(ctx);
|
||||
|
||||
@@ -1,7 +1,65 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { AppleDeviceClass, AppleDeviceFragment } from '../../../../graphql/generated';
|
||||
import { formatDeviceLabel } from '../DeviceUtils';
|
||||
import { ApplePlatform } from '../../appstore/constants';
|
||||
import { filterDevicesForApplePlatform, formatDeviceLabel } from '../DeviceUtils';
|
||||
|
||||
function createDevice(
|
||||
overrides: Partial<AppleDeviceFragment> & Pick<AppleDeviceFragment, 'identifier'>
|
||||
): AppleDeviceFragment {
|
||||
return {
|
||||
__typename: 'AppleDevice',
|
||||
id: uuidv4(),
|
||||
name: 'device',
|
||||
model: null,
|
||||
deviceClass: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
...overrides,
|
||||
} as AppleDeviceFragment;
|
||||
}
|
||||
|
||||
describe(filterDevicesForApplePlatform, () => {
|
||||
const iphone = createDevice({
|
||||
identifier: 'iphone',
|
||||
deviceClass: AppleDeviceClass.Iphone,
|
||||
});
|
||||
const ipad = createDevice({
|
||||
identifier: 'ipad',
|
||||
deviceClass: AppleDeviceClass.Ipad,
|
||||
});
|
||||
const mac = createDevice({
|
||||
identifier: 'mac',
|
||||
deviceClass: AppleDeviceClass.Mac,
|
||||
});
|
||||
const appleTv = createDevice({
|
||||
identifier: 'appletv',
|
||||
model: 'AppleTV6,3',
|
||||
deviceClass: AppleDeviceClass.Unknown,
|
||||
});
|
||||
const unknown = createDevice({
|
||||
identifier: 'unknown',
|
||||
deviceClass: AppleDeviceClass.Unknown,
|
||||
});
|
||||
const noClass = createDevice({
|
||||
identifier: 'noclass',
|
||||
deviceClass: null,
|
||||
});
|
||||
|
||||
it('keeps only iPhone and iPad devices for iOS targets', () => {
|
||||
expect(
|
||||
filterDevicesForApplePlatform(
|
||||
[iphone, ipad, mac, appleTv, unknown, noClass],
|
||||
ApplePlatform.IOS
|
||||
)
|
||||
).toEqual([iphone, ipad]);
|
||||
});
|
||||
|
||||
it('throws for tvOS targets', () => {
|
||||
expect(() =>
|
||||
filterDevicesForApplePlatform([iphone, ipad, mac, appleTv], ApplePlatform.TV_OS)
|
||||
).toThrow('Filtering for tvOS is not supported yet');
|
||||
});
|
||||
});
|
||||
|
||||
describe(formatDeviceLabel, () => {
|
||||
it('returns createdAt clause', async () => {
|
||||
|
||||
+252
-3
@@ -23,17 +23,37 @@ import { ApplePlatform } from '../../appstore/constants';
|
||||
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from '../BuildCredentialsUtils';
|
||||
import { chooseDevicesAsync } from '../DeviceUtils';
|
||||
import { SetUpAdhocProvisioningProfile, doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
|
||||
import { getAscApiKeyForAppSubmissionsAsync } from '../../api/GraphqlClient';
|
||||
import { AuthenticationMode, AppleTeamType } from '../../appstore/authenticateTypes';
|
||||
import { hasAscEnvVars } from '../../appstore/resolveCredentials';
|
||||
import { AppStoreConnectApiKeyQuery } from '../../../../graphql/queries/AppStoreConnectApiKeyQuery';
|
||||
|
||||
jest.mock('../BuildCredentialsUtils');
|
||||
jest.mock('../../../context');
|
||||
jest.mock('../../../ios/api/GraphqlClient');
|
||||
jest.mock('../../../ios/api/GraphqlClient', () => ({
|
||||
...jest.requireActual('../../../ios/api/GraphqlClient'),
|
||||
getAscApiKeyForAppSubmissionsAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../appstore/resolveCredentials', () => ({
|
||||
hasAscEnvVars: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../../graphql/queries/AppStoreConnectApiKeyQuery', () => ({
|
||||
AppStoreConnectApiKeyQuery: {
|
||||
getByIdAsync: jest.fn(),
|
||||
},
|
||||
}));
|
||||
jest.mock('../DeviceUtils', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
...jest.requireActual('../DeviceUtils'),
|
||||
chooseDevicesAsync: jest.fn(),
|
||||
formatDeviceLabel: jest.requireActual('../DeviceUtils').formatDeviceLabel,
|
||||
};
|
||||
});
|
||||
jest.mock('../SetUpDistributionCertificate', () => ({
|
||||
SetUpDistributionCertificate: jest.fn(),
|
||||
}));
|
||||
import { SetUpDistributionCertificate } from '../SetUpDistributionCertificate';
|
||||
jest.mock('../../../../project/ios/target');
|
||||
jest.mock('../../../../prompts');
|
||||
|
||||
@@ -148,6 +168,231 @@ describe('runWithDistributionCertificateAsync', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('runAsync', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.mocked(SetUpDistributionCertificate).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
runAsync: jest.fn().mockResolvedValue({} as AppleDistributionCertificateFragment),
|
||||
}) as any
|
||||
);
|
||||
});
|
||||
|
||||
const setUpAdhocProvisioningProfile = new SetUpAdhocProvisioningProfile({
|
||||
app: { account: {} as Account, projectName: 'projName', bundleIdentifier: 'bundleId' },
|
||||
target: { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} },
|
||||
});
|
||||
|
||||
it('skips non-interactive reuse when refresh is enabled', async () => {
|
||||
const { ctx } = setUpTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
const areBuildCredentialsSetupAsyncSpy = jest
|
||||
.spyOn(SetUpAdhocProvisioningProfile.prototype as any, 'areBuildCredentialsSetupAsync')
|
||||
.mockResolvedValue(true);
|
||||
const ensureAppStoreAuthenticatedForAdhocRefreshAsyncSpy = jest
|
||||
.spyOn(
|
||||
SetUpAdhocProvisioningProfile.prototype as any,
|
||||
'ensureAppStoreAuthenticatedForAdhocRefreshAsync'
|
||||
)
|
||||
.mockResolvedValue(undefined);
|
||||
jest
|
||||
.spyOn(SetUpAdhocProvisioningProfile.prototype, 'runWithDistributionCertificateAsync')
|
||||
.mockResolvedValue({} as IosAppBuildCredentialsFragment);
|
||||
|
||||
await setUpAdhocProvisioningProfile.runAsync(ctx);
|
||||
|
||||
expect(areBuildCredentialsSetupAsyncSpy).not.toHaveBeenCalled();
|
||||
expect(ensureAppStoreAuthenticatedForAdhocRefreshAsyncSpy).toHaveBeenCalledWith(ctx, {
|
||||
account: {} as Account,
|
||||
projectName: 'projName',
|
||||
bundleIdentifier: 'bundleId',
|
||||
});
|
||||
expect(
|
||||
SetUpAdhocProvisioningProfile.prototype.runWithDistributionCertificateAsync
|
||||
).toHaveBeenCalled();
|
||||
expect(getBuildCredentialsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fails when refresh is enabled and credentials are frozen', async () => {
|
||||
const { ctx } = setUpTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
Object.defineProperty(ctx, 'freezeCredentials', { value: true });
|
||||
|
||||
await expect(setUpAdhocProvisioningProfile.runAsync(ctx)).rejects.toThrow(
|
||||
'Cannot refresh ad-hoc provisioning profile when credentials are frozen'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh ad-hoc provisioning profile', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
jest.mocked(getApplePlatformFromTarget).mockReturnValue(ApplePlatform.IOS);
|
||||
jest.mocked(SetUpDistributionCertificate).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
runAsync: jest.fn().mockResolvedValue({} as AppleDistributionCertificateFragment),
|
||||
}) as any
|
||||
);
|
||||
jest.mocked(hasAscEnvVars).mockReturnValue(false);
|
||||
jest.mocked(getAscApiKeyForAppSubmissionsAsync).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
const setUpAdhocProvisioningProfile = new SetUpAdhocProvisioningProfile({
|
||||
app: { account: {} as Account, projectName: 'projName', bundleIdentifier: 'bundleId' },
|
||||
target: { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} },
|
||||
});
|
||||
|
||||
it('provisions all registered devices without prompting in refresh mode', async () => {
|
||||
const { ctx, distCert } = setUpRefreshTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
|
||||
await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(ctx, distCert);
|
||||
|
||||
expect(chooseDevicesAsync).not.toHaveBeenCalled();
|
||||
expect(ctx.appStore.createOrReuseAdhocProvisioningProfileAsync).toHaveBeenCalledWith(
|
||||
['id1', 'id2', 'id3'],
|
||||
'bundleId',
|
||||
distCert.serialNumber,
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
|
||||
it('filters devices by target platform only in refresh mode', async () => {
|
||||
const { ctx, distCert } = setUpRefreshTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
ctx.ios.getDevicesForAppleTeamAsync = jest.fn().mockResolvedValue([
|
||||
{ identifier: 'id1', model: 'iPhone17,1', deviceClass: AppleDeviceClass.Iphone },
|
||||
{ identifier: 'id2', model: 'AppleTV6,3', deviceClass: AppleDeviceClass.Unknown },
|
||||
{ identifier: 'id3', deviceClass: AppleDeviceClass.Mac },
|
||||
]);
|
||||
|
||||
await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(ctx, distCert);
|
||||
|
||||
expect(ctx.appStore.createOrReuseAdhocProvisioningProfileAsync).toHaveBeenCalledWith(
|
||||
['id1'],
|
||||
'bundleId',
|
||||
distCert.serialNumber,
|
||||
expect.anything()
|
||||
);
|
||||
expect(chooseDevicesAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('continues without prompting when some devices are not provisioned in refresh mode', async () => {
|
||||
const { ctx, distCert } = setUpRefreshTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
|
||||
provisioningProfile: {
|
||||
appleTeam: { appleTeamIdentifier: 'team' },
|
||||
appleDevices: [{ identifier: 'id1' }],
|
||||
developerPortalIdentifier: 'provisioningProfileId',
|
||||
},
|
||||
} as IosAppBuildCredentialsFragment);
|
||||
ctx.ios.updateProvisioningProfileAsync = jest.fn().mockResolvedValue({
|
||||
appleTeam: { appleTeamIdentifier: 'team' },
|
||||
appleDevices: [{ identifier: 'id1' }],
|
||||
developerPortalIdentifier: 'provisioningProfileId',
|
||||
});
|
||||
|
||||
await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(ctx, distCert);
|
||||
|
||||
expect(selectAsync).not.toHaveBeenCalled();
|
||||
expect(assignBuildCredentialsAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('errors when no devices are registered in non-interactive mode', async () => {
|
||||
const { ctx, distCert } = setUpRefreshTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
ctx.ios.getDevicesForAppleTeamAsync = jest.fn().mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(ctx, distCert)
|
||||
).rejects.toThrow(
|
||||
'No devices are registered for this Apple team. Register devices with eas device:create first.'
|
||||
);
|
||||
});
|
||||
|
||||
describe('ensureAppStoreAuthenticatedForAdhocRefreshAsync', () => {
|
||||
it('authenticates with ASC environment variables when present', async () => {
|
||||
const { ctx } = setUpRefreshTest();
|
||||
jest.mocked(hasAscEnvVars).mockReturnValue(true);
|
||||
jest
|
||||
.spyOn(SetUpAdhocProvisioningProfile.prototype, 'runWithDistributionCertificateAsync')
|
||||
.mockResolvedValue({} as IosAppBuildCredentialsFragment);
|
||||
|
||||
await setUpAdhocProvisioningProfile.runAsync(ctx);
|
||||
|
||||
expect(ctx.appStore.ensureAuthenticatedAsync).toHaveBeenCalledWith({
|
||||
mode: AuthenticationMode.API_KEY,
|
||||
});
|
||||
expect(getAscApiKeyForAppSubmissionsAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('authenticates with the stored submissions ASC API key when env vars are absent', async () => {
|
||||
const { ctx } = setUpRefreshTest();
|
||||
jest.mocked(hasAscEnvVars).mockReturnValue(false);
|
||||
jest.mocked(getAscApiKeyForAppSubmissionsAsync).mockResolvedValue({
|
||||
id: 'asc-key-id',
|
||||
appleTeam: {
|
||||
appleTeamIdentifier: 'TEAM123',
|
||||
appleTeamName: 'Team Name',
|
||||
},
|
||||
} as any);
|
||||
jest.mocked(AppStoreConnectApiKeyQuery.getByIdAsync).mockResolvedValue({
|
||||
keyP8: 'key-p8',
|
||||
keyIdentifier: 'key-id',
|
||||
issuerIdentifier: 'issuer-id',
|
||||
} as any);
|
||||
jest
|
||||
.spyOn(SetUpAdhocProvisioningProfile.prototype, 'runWithDistributionCertificateAsync')
|
||||
.mockResolvedValue({} as IosAppBuildCredentialsFragment);
|
||||
|
||||
await setUpAdhocProvisioningProfile.runAsync(ctx);
|
||||
|
||||
expect(ctx.appStore.ensureAuthenticatedAsync).toHaveBeenCalledWith({
|
||||
mode: AuthenticationMode.API_KEY,
|
||||
ascApiKey: {
|
||||
keyP8: 'key-p8',
|
||||
keyId: 'key-id',
|
||||
issuerId: 'issuer-id',
|
||||
},
|
||||
teamId: 'TEAM123',
|
||||
teamName: 'Team Name',
|
||||
teamType: AppleTeamType.COMPANY_OR_ORGANIZATION,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setUpRefreshTest(): {
|
||||
ctx: CredentialsContext;
|
||||
distCert: AppleDistributionCertificateFragment;
|
||||
} {
|
||||
const { ctx, distCert } = setUpTest();
|
||||
Object.defineProperty(ctx, 'nonInteractive', { value: true });
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', { value: true });
|
||||
distCert.appleTeam = {
|
||||
appleTeamIdentifier: 'team',
|
||||
} as AppleDistributionCertificateFragment['appleTeam'];
|
||||
distCert.serialNumber = 'serial-number';
|
||||
ctx.appStore.ensureAuthenticatedAsync = jest.fn().mockResolvedValue({});
|
||||
jest.mocked(getBuildCredentialsAsync).mockResolvedValue(null);
|
||||
ctx.ios.getDevicesForAppleTeamAsync = jest.fn().mockResolvedValue([
|
||||
{ identifier: 'id1', deviceClass: AppleDeviceClass.Iphone },
|
||||
{ identifier: 'id2', deviceClass: AppleDeviceClass.Ipad },
|
||||
{ identifier: 'id3', deviceClass: AppleDeviceClass.Iphone },
|
||||
] as AppleDeviceFragment[]);
|
||||
return { ctx, distCert };
|
||||
}
|
||||
|
||||
function setUpTest(): { ctx: CredentialsContext; distCert: AppleDistributionCertificateFragment } {
|
||||
const ctx = jest.mocked(
|
||||
new CredentialsContext(
|
||||
@@ -177,8 +422,7 @@ function setUpTest(): { ctx: CredentialsContext; distCert: AppleDistributionCert
|
||||
{ identifier: 'id2', name: 'Device 2', deviceClass: AppleDeviceClass.Iphone },
|
||||
{ identifier: 'id3', name: 'Device 3', deviceClass: AppleDeviceClass.Mac },
|
||||
] as AppleDevice[]);
|
||||
// @ts-expect-error
|
||||
jest.mocked(getApplePlatformFromTarget).mockResolvedValue(ApplePlatform.IOS);
|
||||
jest.mocked(getApplePlatformFromTarget).mockReturnValue(ApplePlatform.IOS);
|
||||
Object.defineProperty(ctx, 'appStore', { value: jest.mock('../../appstore/AppStoreApi') });
|
||||
ctx.appStore.createOrReuseAdhocProvisioningProfileAsync = jest.fn().mockResolvedValue({
|
||||
provisioningProfileId: 'provisioningProfileId',
|
||||
@@ -186,6 +430,11 @@ function setUpTest(): { ctx: CredentialsContext; distCert: AppleDistributionCert
|
||||
ctx.ios.createOrGetExistingAppleAppIdentifierAsync = jest
|
||||
.fn()
|
||||
.mockResolvedValue({} as AppleAppIdentifierFragment);
|
||||
ctx.ios.createProvisioningProfileAsync = jest.fn().mockResolvedValue({
|
||||
appleTeam: { appleTeamIdentifier: 'team' },
|
||||
appleDevices: [{ identifier: 'id1' }, { identifier: 'id2' }, { identifier: 'id3' }],
|
||||
developerPortalIdentifier: 'provisioningProfileId',
|
||||
});
|
||||
jest.mocked(assignBuildCredentialsAsync).mockResolvedValue({} as IosAppBuildCredentialsFragment);
|
||||
const distCert = {} as AppleDistributionCertificateFragment;
|
||||
return { ctx, distCert };
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { EasJson } from '@expo/eas-json';
|
||||
|
||||
import { Analytics } from '../../../../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Account, IosAppBuildCredentialsFragment } from '../../../../graphql/generated';
|
||||
import { Actor } from '../../../../user/User';
|
||||
import { Client } from '../../../../vcs/vcs';
|
||||
import { CredentialsContext, CredentialsContextProjectInfo } from '../../../context';
|
||||
import { SetUpAdhocProvisioningProfile } from '../SetUpAdhocProvisioningProfile';
|
||||
import { SetUpInternalProvisioningProfile } from '../SetUpInternalProvisioningProfile';
|
||||
import { SetUpTargetBuildCredentials } from '../SetUpTargetBuildCredentials';
|
||||
|
||||
jest.mock('../SetUpAdhocProvisioningProfile', () => ({
|
||||
SetUpAdhocProvisioningProfile: jest.fn(),
|
||||
}));
|
||||
jest.mock('../SetUpInternalProvisioningProfile', () => ({
|
||||
SetUpInternalProvisioningProfile: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../context');
|
||||
|
||||
describe(SetUpTargetBuildCredentials, () => {
|
||||
const app = {
|
||||
account: { name: 'account' } as Account,
|
||||
projectName: 'projName',
|
||||
bundleIdentifier: 'bundleId',
|
||||
};
|
||||
const target = { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} };
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(SetUpAdhocProvisioningProfile).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
runAsync: jest.fn().mockResolvedValue({} as IosAppBuildCredentialsFragment),
|
||||
}) as any
|
||||
);
|
||||
jest.mocked(SetUpInternalProvisioningProfile).mockImplementation(
|
||||
() =>
|
||||
({
|
||||
runAsync: jest.fn().mockResolvedValue({} as IosAppBuildCredentialsFragment),
|
||||
}) as any
|
||||
);
|
||||
});
|
||||
|
||||
function createCtx(overrides: Partial<CredentialsContext> = {}): CredentialsContext {
|
||||
const ctx = jest.mocked(
|
||||
new CredentialsContext(
|
||||
{} as {
|
||||
projectInfo: CredentialsContextProjectInfo | null;
|
||||
easJsonCliConfig?: EasJson['cli'];
|
||||
nonInteractive: boolean;
|
||||
projectDir: string;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
env?: Env;
|
||||
vcsClient: Client;
|
||||
}
|
||||
)
|
||||
);
|
||||
Object.defineProperty(ctx, 'refreshAdHocProvisioningProfile', {
|
||||
value: true,
|
||||
writable: true,
|
||||
});
|
||||
return Object.assign(ctx, overrides);
|
||||
}
|
||||
|
||||
it('errors when refresh is enabled for store distribution builds', async () => {
|
||||
const action = new SetUpTargetBuildCredentials({
|
||||
app,
|
||||
distribution: 'store',
|
||||
entitlements: {},
|
||||
target,
|
||||
});
|
||||
|
||||
await expect(action.setupBuildCredentialsAsync(createCtx())).rejects.toThrow(
|
||||
'--refresh-ad-hoc-provisioning-profile is only supported for internal distribution builds.'
|
||||
);
|
||||
});
|
||||
|
||||
it('errors when refresh is enabled for universal enterprise internal builds', async () => {
|
||||
const action = new SetUpTargetBuildCredentials({
|
||||
app,
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: 'universal',
|
||||
entitlements: {},
|
||||
target,
|
||||
});
|
||||
|
||||
await expect(action.setupBuildCredentialsAsync(createCtx())).rejects.toThrow(
|
||||
'--refresh-ad-hoc-provisioning-profile is only supported for ad-hoc internal builds.'
|
||||
);
|
||||
});
|
||||
|
||||
it('delegates to internal provisioning setup when refresh is enabled for internal ad-hoc builds', async () => {
|
||||
const action = new SetUpTargetBuildCredentials({
|
||||
app,
|
||||
distribution: 'internal',
|
||||
entitlements: {},
|
||||
target,
|
||||
});
|
||||
const ctx = createCtx();
|
||||
|
||||
await action.setupBuildCredentialsAsync(ctx);
|
||||
|
||||
expect(SetUpInternalProvisioningProfile).toHaveBeenCalledWith({ app, target });
|
||||
expect(SetUpAdhocProvisioningProfile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+85
@@ -1458,6 +1458,7 @@ export type App = Project & {
|
||||
/** @deprecated Classic updates have been deprecated. */
|
||||
description: Scalars['String']['output'];
|
||||
devDomainName?: Maybe<AppDevDomainName>;
|
||||
deviceRunSessionsPaginated: AppDeviceRunSessionsConnection;
|
||||
/** Environment secrets for an app */
|
||||
environmentSecrets: Array<EnvironmentSecret>;
|
||||
environmentVariableEnvironments: Array<Scalars['EnvironmentVariableEnvironment']['output']>;
|
||||
@@ -1686,6 +1687,16 @@ export type AppDeploymentsArgs = {
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppDeviceRunSessionsPaginatedArgs = {
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
before?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<DeviceRunSessionFilterInput>;
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
last?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
|
||||
/** Represents an Exponent App (or Experience in legacy terms) */
|
||||
export type AppEnvironmentSecretsArgs = {
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']>>;
|
||||
@@ -2004,6 +2015,18 @@ export type AppDevDomainNameMutationChangeDevDomainNameArgs = {
|
||||
name: Scalars['DevDomainName']['input'];
|
||||
};
|
||||
|
||||
export type AppDeviceRunSessionEdge = {
|
||||
__typename?: 'AppDeviceRunSessionEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
node: DeviceRunSession;
|
||||
};
|
||||
|
||||
export type AppDeviceRunSessionsConnection = {
|
||||
__typename?: 'AppDeviceRunSessionsConnection';
|
||||
edges: Array<AppDeviceRunSessionEdge>;
|
||||
pageInfo: PageInfo;
|
||||
};
|
||||
|
||||
export type AppFingerprintEdge = {
|
||||
__typename?: 'AppFingerprintEdge';
|
||||
cursor: Scalars['String']['output'];
|
||||
@@ -2147,6 +2170,15 @@ export type AppObserve = {
|
||||
navigationRoutes: AppObserveNavigationRoutesConnection;
|
||||
timeSeries: AppObserveTimeSeries;
|
||||
totalEventCount: Scalars['Int']['output'];
|
||||
/**
|
||||
* Approximate count of unique users (`eas_client_id`) with at least one
|
||||
* supported-metric event in `[startTime, endTime)`. Uses ClickHouse's
|
||||
* HyperLogLog `uniq()`, so the value can drift by a small percent on apps
|
||||
* with very large user bases. `metricNames` on the input is currently
|
||||
* ignored: the count is always over all supported metrics, matching the
|
||||
* universe used by `appVersions` headline counts.
|
||||
*/
|
||||
uniqueActiveUserCount: Scalars['Int']['output'];
|
||||
updates: AppObserveUpdatesConnection;
|
||||
};
|
||||
|
||||
@@ -2216,6 +2248,11 @@ export type AppObserveTimeSeriesArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveUniqueActiveUserCountArgs = {
|
||||
input: AppObserveReleasesInput;
|
||||
};
|
||||
|
||||
|
||||
export type AppObserveUpdatesArgs = {
|
||||
input: AppObserveUpdatesInput;
|
||||
};
|
||||
@@ -2224,6 +2261,11 @@ export type AppObserveAppBuildEmbeddedSummary = {
|
||||
__typename?: 'AppObserveAppBuildEmbeddedSummary';
|
||||
eventCount: Scalars['Int']['output'];
|
||||
firstSeenAt: Scalars['DateTime']['output'];
|
||||
/**
|
||||
* Unique users whose most recent supported-metric event in the queried
|
||||
* range ran this build's embedded bundle.
|
||||
*/
|
||||
lastSeenUserCount: Scalars['Int']['output'];
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
@@ -2239,7 +2281,18 @@ export type AppObserveAppBuildNumber = {
|
||||
embedded?: Maybe<AppObserveAppBuildEmbeddedSummary>;
|
||||
eventCount: Scalars['Int']['output'];
|
||||
firstSeenAt: Scalars['DateTime']['output'];
|
||||
/**
|
||||
* Unique users whose most recent supported-metric event in the queried
|
||||
* range was on this (appVersion, appBuildNumber) tuple.
|
||||
*/
|
||||
lastSeenUserCount: Scalars['Int']['output'];
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
/**
|
||||
* Updates seen on this build number in the queried time range. Each
|
||||
* entry's counts and EAS builds are scoped to this (appVersion,
|
||||
* appBuildNumber, appUpdateId) tuple.
|
||||
*/
|
||||
updates: Array<AppObserveAppUpdate>;
|
||||
};
|
||||
|
||||
export type AppObserveAppEasBuild = {
|
||||
@@ -2257,6 +2310,11 @@ export type AppObserveAppUpdate = {
|
||||
easBuilds: Array<AppObserveAppEasBuild>;
|
||||
eventCount: Scalars['Int']['output'];
|
||||
firstSeenAt: Scalars['DateTime']['output'];
|
||||
/**
|
||||
* Unique users whose most recent supported-metric event in the queried
|
||||
* range matched this update at its current nesting level.
|
||||
*/
|
||||
lastSeenUserCount: Scalars['Int']['output'];
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
@@ -2266,6 +2324,17 @@ export type AppObserveAppVersion = {
|
||||
buildNumbers: Array<AppObserveAppBuildNumber>;
|
||||
eventCount: Scalars['Int']['output'];
|
||||
firstSeenAt: Scalars['DateTime']['output'];
|
||||
/**
|
||||
* Unique users whose most recent supported-metric event in
|
||||
* `[startTime, endTime)` was on this version. Each active user
|
||||
* contributes to exactly one version, so summing this field across the
|
||||
* returned versions yields the total active users in the window (equal
|
||||
* to `AppObserve.uniqueActiveUserCount` up to HyperLogLog noise).
|
||||
* A version may be returned with `lastSeenUserCount = 0` when
|
||||
* every user who touched it later moved to a newer version. Users with
|
||||
* no events in the window are not counted on any version.
|
||||
*/
|
||||
lastSeenUserCount: Scalars['Int']['output'];
|
||||
metrics: Array<AppObserveAppVersionMetric>;
|
||||
uniqueUserCount: Scalars['Int']['output'];
|
||||
updates: Array<AppObserveAppUpdate>;
|
||||
@@ -5237,6 +5306,12 @@ export type DeviceRunSession = {
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
};
|
||||
|
||||
export type DeviceRunSessionFilterInput = {
|
||||
platforms?: InputMaybe<Array<AppPlatform>>;
|
||||
statuses?: InputMaybe<Array<DeviceRunSessionStatus>>;
|
||||
types?: InputMaybe<Array<DeviceRunSessionType>>;
|
||||
};
|
||||
|
||||
export type DeviceRunSessionMutation = {
|
||||
__typename?: 'DeviceRunSessionMutation';
|
||||
/** Create a device run session */
|
||||
@@ -12908,6 +12983,16 @@ export type DeviceRunSessionByIdQueryVariables = Exact<{
|
||||
|
||||
export type DeviceRunSessionByIdQuery = { __typename?: 'RootQuery', deviceRunSessions: { __typename?: 'DeviceRunSessionQuery', byId: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, type: DeviceRunSessionType, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, remoteConfig?: { __typename: 'AgentDeviceRunSessionRemoteConfig', agentDeviceRemoteSessionUrl: string, agentDeviceRemoteSessionToken: string, webPreviewUrl?: string | null } | { __typename: 'ArgentRunSessionRemoteConfig', toolsUrl: string, webPreviewUrl?: string | null } | { __typename: 'ServeSimRunSessionRemoteConfig', previewUrl: string, streamUrl: string } | null, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus } | null } } };
|
||||
|
||||
export type DeviceRunSessionsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
first?: InputMaybe<Scalars['Int']['input']>;
|
||||
after?: InputMaybe<Scalars['String']['input']>;
|
||||
filter?: InputMaybe<DeviceRunSessionFilterInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type DeviceRunSessionsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, deviceRunSessionsPaginated: { __typename?: 'AppDeviceRunSessionsConnection', edges: Array<{ __typename?: 'AppDeviceRunSessionEdge', cursor: string, node: { __typename?: 'DeviceRunSession', id: string, status: DeviceRunSessionStatus, type: DeviceRunSessionType, platform: AppPlatform, createdAt: any, startedAt?: any | null, finishedAt?: any | null, app: { __typename?: 'App', id: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, turtleJobRun?: { __typename?: 'JobRun', id: string, status: JobRunStatus } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null } } } } };
|
||||
|
||||
export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
@@ -2,7 +2,13 @@ import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import { DeviceRunSessionByIdQuery, DeviceRunSessionByIdQueryVariables } from '../generated';
|
||||
import {
|
||||
DeviceRunSessionByIdQuery,
|
||||
DeviceRunSessionByIdQueryVariables,
|
||||
DeviceRunSessionFilterInput,
|
||||
DeviceRunSessionsByAppIdQuery,
|
||||
DeviceRunSessionsByAppIdQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const DeviceRunSessionQuery = {
|
||||
async byIdAsync(
|
||||
@@ -58,4 +64,74 @@ export const DeviceRunSessionQuery = {
|
||||
);
|
||||
return data.deviceRunSessions.byId;
|
||||
},
|
||||
async listByAppIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
first,
|
||||
after,
|
||||
filter,
|
||||
}: {
|
||||
appId: string;
|
||||
first?: number;
|
||||
after?: string;
|
||||
filter?: DeviceRunSessionFilterInput;
|
||||
}
|
||||
): Promise<DeviceRunSessionsByAppIdQuery['app']['byId']['deviceRunSessionsPaginated']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<DeviceRunSessionsByAppIdQuery, DeviceRunSessionsByAppIdQueryVariables>(
|
||||
gql`
|
||||
query DeviceRunSessionsByAppId(
|
||||
$appId: String!
|
||||
$first: Int
|
||||
$after: String
|
||||
$filter: DeviceRunSessionFilterInput
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
deviceRunSessionsPaginated(first: $first, after: $after, filter: $filter) {
|
||||
edges {
|
||||
cursor
|
||||
node {
|
||||
id
|
||||
status
|
||||
type
|
||||
platform
|
||||
createdAt
|
||||
startedAt
|
||||
finishedAt
|
||||
app {
|
||||
id
|
||||
slug
|
||||
ownerAccount {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
turtleJobRun {
|
||||
id
|
||||
status
|
||||
}
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId, first, after, filter },
|
||||
{ requestPolicy: 'network-only' }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.app.byId.deviceRunSessionsPaginated;
|
||||
},
|
||||
};
|
||||
@@ -28,6 +28,7 @@ function makeAppVersion(
|
||||
eventCount: metrics.reduce((sum, m) => sum + m.eventCount, 0),
|
||||
uniqueUserCount: 50,
|
||||
firstSeenAt: '2025-01-01T00:00:00.000Z',
|
||||
lastSeenUserCount: 12,
|
||||
buildNumbers: [],
|
||||
updates: [],
|
||||
metrics: metrics.map(m => ({
|
||||
|
||||
@@ -20,7 +20,7 @@ export async function getManagedApplicationTargetEntitlementsAsync(
|
||||
env: Record<string, string>,
|
||||
vcsClient: Client
|
||||
): Promise<JSONObject> {
|
||||
let expoConfigError: Error | undefined;
|
||||
let expoConfigError: any;
|
||||
if (isExpoInstalled(projectDir)) {
|
||||
try {
|
||||
const { stdout } = await spawnExpoCommand(
|
||||
@@ -45,15 +45,22 @@ export async function getManagedApplicationTargetEntitlementsAsync(
|
||||
scope.setTag('build_id', process.env.EAS_BUILD_ID);
|
||||
}
|
||||
scope.setTag('config_resolution', 'ios_entitlements_introspection');
|
||||
scope.setExtra('message', 'iOS entitlements config fallback');
|
||||
scope.setExtra('stdout', error.stdout);
|
||||
scope.setExtra('stderr', error.stderr);
|
||||
Sentry.captureException(error);
|
||||
scope.setExtra(
|
||||
'expo_config_command_error',
|
||||
JSON.stringify({
|
||||
message: error.message,
|
||||
output: error.output,
|
||||
signal: error.signal,
|
||||
status: error.status,
|
||||
stderr: error.stderr,
|
||||
stdout: error.stdout,
|
||||
})
|
||||
);
|
||||
Sentry.captureMessage('iOS entitlements config fallback', 'error');
|
||||
});
|
||||
} catch {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
expoConfigError = error;
|
||||
Log.warn(
|
||||
`Failed to read the app config from the project using the local Expo CLI: ${formatError(error)}`
|
||||
@@ -110,7 +117,7 @@ async function resolveManagedApplicationTargetEntitlementsWithBundledConfigAsync
|
||||
}
|
||||
}
|
||||
|
||||
function formatError(error: Error & { stderr?: string; stdout?: string }): string {
|
||||
function formatError(error: any): string {
|
||||
return error.stderr?.trim() || error.stdout?.trim() || error.message;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { DeviceRunSessionByIdQuery } from '../graphql/generated';
|
||||
import { DeviceRunSessionByIdQuery, DeviceRunSessionType } from '../graphql/generated';
|
||||
|
||||
type DeviceRunSessionByIdResult = DeviceRunSessionByIdQuery['deviceRunSessions']['byId'];
|
||||
export type DeviceRunSessionRemoteConfig = NonNullable<DeviceRunSessionByIdResult['remoteConfig']>;
|
||||
|
||||
// Mapping enum -> CLI flag value. Declared as Record<DeviceRunSessionType, string>
|
||||
// so adding a new enum value in codegen fails the build until it is wired up here.
|
||||
export const DEVICE_RUN_SESSION_TYPE_FLAG_VALUES: Record<DeviceRunSessionType, string> = {
|
||||
[DeviceRunSessionType.AgentDevice]: 'agent-device',
|
||||
[DeviceRunSessionType.Argent]: 'argent',
|
||||
[DeviceRunSessionType.ServeSim]: 'serve-sim',
|
||||
};
|
||||
|
||||
export const DEVICE_RUN_SESSION_TYPE_BY_FLAG_VALUE = Object.fromEntries(
|
||||
(Object.entries(DEVICE_RUN_SESSION_TYPE_FLAG_VALUES) as [DeviceRunSessionType, string][]).map(
|
||||
([type, value]) => [value, type]
|
||||
)
|
||||
) as Record<string, DeviceRunSessionType>;
|
||||
|
||||
export function deviceRunSessionTypeToFlagValue(type: DeviceRunSessionType): string {
|
||||
return DEVICE_RUN_SESSION_TYPE_FLAG_VALUES[type];
|
||||
}
|
||||
|
||||
export function formatRemoteSessionInstructions(
|
||||
remoteConfig: DeviceRunSessionRemoteConfig
|
||||
): string {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.23.5",
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "11.2.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli-local-build-plugin",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"description": "Tool for running EAS compatible builds on a local machine.",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
@@ -26,8 +26,8 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/build-tools": "19.0.0",
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/build-tools": "19.1.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"@expo/logger": "19.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"@expo/turtle-spawn": "19.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/steps",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"license": "BUSL-1.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
@@ -27,7 +27,7 @@
|
||||
"clean": "rimraf node_modules dist coverage \"*.tsbuildinfo\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"@expo/logger": "19.0.0",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"arg": "^5.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@expo/worker",
|
||||
"version": "19.0.0",
|
||||
"version": "19.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "yarn watch",
|
||||
@@ -19,16 +19,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "7.24.7",
|
||||
"@expo/build-tools": "19.0.0",
|
||||
"@expo/build-tools": "19.1.0",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
"@expo/config-types": "55.0.5",
|
||||
"@expo/downloader": "19.0.0",
|
||||
"@expo/eas-build-job": "19.0.0",
|
||||
"@expo/eas-build-job": "19.1.0",
|
||||
"@expo/logger": "19.0.0",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "19.0.0",
|
||||
"@expo/steps": "19.1.0",
|
||||
"@expo/template-file": "19.0.0",
|
||||
"@expo/turtle-spawn": "19.0.0",
|
||||
"@hapi/boom": "10.0.1",
|
||||
|
||||
@@ -49,10 +49,7 @@ export class WsHelper {
|
||||
public onMessage(cb: (message: any) => void): void {
|
||||
this.onMessageCb = jest.fn(raw => {
|
||||
logger.debug({ raw }, 'ws message');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed.type !== 'build-phase-stats') {
|
||||
cb(parsed);
|
||||
}
|
||||
cb(JSON.parse(raw));
|
||||
});
|
||||
this.ws.on('message', this.onMessageCb);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import os from 'os';
|
||||
|
||||
import config from '../config';
|
||||
import { getBuildEnv } from '../env';
|
||||
import { getBuildEnv, getGradleMemoryOptions } from '../env';
|
||||
|
||||
describe(getBuildEnv.name, () => {
|
||||
const originalSentryDsn = config.sentry.dsn;
|
||||
|
||||
afterEach(() => {
|
||||
config.sentry.dsn = originalSentryDsn;
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('passes the CLI sentry DSN to worker child processes', () => {
|
||||
@@ -59,4 +61,26 @@ describe(getBuildEnv.name, () => {
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_BASE_URL).toBeUndefined();
|
||||
expect(env.EXPO_PRECOMPILED_MODULES_PATH).toBeUndefined();
|
||||
});
|
||||
|
||||
it('sizes Gradle memory options from total memory', () => {
|
||||
const totalMemory = jest.spyOn(os, 'totalmem');
|
||||
|
||||
totalMemory.mockReturnValue(16 * 1024 ** 3);
|
||||
expect(getGradleMemoryOptions()).toEqual({
|
||||
maxHeapSize: '4g',
|
||||
});
|
||||
|
||||
totalMemory.mockReturnValue(32 * 1024 ** 3);
|
||||
expect(getGradleMemoryOptions()).toEqual({
|
||||
maxHeapSize: '8g',
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Gradle memory options conservative on low-memory runners', () => {
|
||||
jest.spyOn(os, 'totalmem').mockReturnValue(4 * 1024 ** 3);
|
||||
|
||||
expect(getGradleMemoryOptions()).toEqual({
|
||||
maxHeapSize: '1g',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Datadog } from '@expo/build-tools';
|
||||
import { BuildPhase, BuildPhaseResult, Job, errors } from '@expo/eas-build-job';
|
||||
import { Job, errors } from '@expo/eas-build-job';
|
||||
import spawn from '@expo/turtle-spawn';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
@@ -105,42 +105,6 @@ describe(getExpoPackageVersionAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('BuildService.reportBuildPhaseStats', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sends phase stats to the launcher websocket', () => {
|
||||
const send = jest.fn();
|
||||
const service = new BuildService();
|
||||
service.setWS({ send } as any);
|
||||
|
||||
const stats = {
|
||||
buildPhase: BuildPhase.RUN_FASTLANE,
|
||||
result: BuildPhaseResult.SUCCESS,
|
||||
durationMs: 1234,
|
||||
};
|
||||
service.reportBuildPhaseStats(stats);
|
||||
|
||||
expect(send).toHaveBeenCalledWith({
|
||||
type: 'build-phase-stats',
|
||||
...stats,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not send phase stats when there is no launcher websocket', () => {
|
||||
const service = new BuildService();
|
||||
|
||||
service.reportBuildPhaseStats({
|
||||
buildPhase: BuildPhase.PREPARE_PROJECT,
|
||||
result: BuildPhaseResult.SUCCESS,
|
||||
durationMs: 1,
|
||||
});
|
||||
|
||||
expect(datadogSetupMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BuildService Datadog setup', () => {
|
||||
const buildLogger = { child: jest.fn(() => buildLogger) } as any;
|
||||
|
||||
@@ -175,11 +139,7 @@ describe('BuildService Datadog setup', () => {
|
||||
turtleBuildId: 'build-id',
|
||||
robotAccessToken: 'token-abc',
|
||||
});
|
||||
expect(createBuildContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reportBuildPhaseStatsFn: expect.any(Function),
|
||||
})
|
||||
);
|
||||
expect(createBuildContextMock).toHaveBeenCalled();
|
||||
expect(datadogFlushAsyncMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -201,11 +161,7 @@ describe('BuildService Datadog setup', () => {
|
||||
turtleBuildId: 'build-id',
|
||||
robotAccessToken: 'token-abc',
|
||||
});
|
||||
expect(createBuildContextMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
reportBuildPhaseStatsFn: expect.any(Function),
|
||||
})
|
||||
);
|
||||
expect(createBuildContextMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('leaves Datadog unconfigured when the robot access token is unavailable', async () => {
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
Artifacts,
|
||||
BuildContext,
|
||||
Builders,
|
||||
Sentry,
|
||||
parseGradleProfile,
|
||||
formatGradleProfileReport,
|
||||
runGenericJobAsync,
|
||||
} from '@expo/build-tools';
|
||||
import { Artifacts, BuildContext, Builders, runGenericJobAsync } from '@expo/build-tools';
|
||||
import {
|
||||
Android,
|
||||
BuildJob,
|
||||
@@ -21,8 +13,6 @@ import {
|
||||
} from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import omit from 'lodash/omit';
|
||||
import path from 'path';
|
||||
|
||||
import config from './config';
|
||||
import { displayWorkerRuntimeInfo } from './displayRuntimeInfo';
|
||||
import { Analytics, Event, logProjectDependenciesAsync } from './external/analytics';
|
||||
@@ -92,21 +82,6 @@ export async function build({
|
||||
|
||||
analytics.logEvent(Event.WORKER_BUILD_SUCCESS, {});
|
||||
|
||||
if (job.platform === Platform.ANDROID) {
|
||||
try {
|
||||
await ctx.runBuildPhase(BuildPhase.GRADLE_BUILD_PROFILE, async () => {
|
||||
const androidDir = path.join(ctx.getReactNativeProjectDirectory(), 'android');
|
||||
const profileTasks = await parseGradleProfile(androidDir);
|
||||
if (profileTasks.length > 0) {
|
||||
const report = formatGradleProfileReport(profileTasks);
|
||||
ctx.logger.info(report);
|
||||
}
|
||||
});
|
||||
} catch (err: any) {
|
||||
Sentry.capture('Failed to parse Gradle build profile', err);
|
||||
}
|
||||
}
|
||||
|
||||
return artifacts;
|
||||
} catch (err: any) {
|
||||
if ('mode' in job && ![BuildMode.CUSTOM, BuildMode.REPACK].includes(job.mode)) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { GCS, GCSLoggerStream } from '@expo/build-tools';
|
||||
import path from 'path';
|
||||
|
||||
import { Environment } from './constants';
|
||||
import { ResourceClass, Worker } from './external/turtle';
|
||||
import { Worker } from './external/turtle';
|
||||
import env from './utils/env';
|
||||
|
||||
type ReplaceUndefinedWithNull<T> = undefined extends T ? Exclude<T, undefined> | null : T;
|
||||
@@ -102,10 +102,6 @@ export default {
|
||||
transform: createBase64EnvTransformer('runMetricsServer'),
|
||||
defaultValue: null,
|
||||
}),
|
||||
resourceClass: env<ResourceClass | null>('WORKER_RUNTIME_CONFIG_BASE64', {
|
||||
transform: createBase64EnvTransformer('resourceClass'),
|
||||
defaultValue: null,
|
||||
}),
|
||||
buildId: env<string>('WORKER_RUNTIME_CONFIG_BASE64', {
|
||||
transform: createBase64EnvTransformer('buildId'),
|
||||
}),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BuildContext, BuildContextOptions, LogBuffer } from '@expo/build-tools';
|
||||
import { BuildPhaseStats, Job, ManagedArtifactType, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { Job, ManagedArtifactType, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { bunyan } from '@expo/logger';
|
||||
import assert from 'assert';
|
||||
|
||||
@@ -24,7 +24,6 @@ export function createBuildContext<TJob extends Job>({
|
||||
projectId,
|
||||
buildId,
|
||||
buildLogger,
|
||||
reportBuildPhaseStatsFn,
|
||||
}: {
|
||||
job: TJob;
|
||||
logBuffer: LogBuffer;
|
||||
@@ -33,7 +32,6 @@ export function createBuildContext<TJob extends Job>({
|
||||
projectId: string;
|
||||
buildId: string;
|
||||
buildLogger: bunyan;
|
||||
reportBuildPhaseStatsFn: (stats: BuildPhaseStats) => void;
|
||||
}): BuildContext<TJob> {
|
||||
const env = getBuildEnv({ job, projectId, metadata, buildId });
|
||||
const childLogger = buildLogger.child({ buildId });
|
||||
@@ -89,7 +87,6 @@ export function createBuildContext<TJob extends Job>({
|
||||
},
|
||||
cacheManager: new GCSCacheManager(),
|
||||
metadata,
|
||||
reportBuildPhaseStats: reportBuildPhaseStatsFn,
|
||||
expoApiV2BaseUrl: config.wwwApiV2BaseUrl,
|
||||
});
|
||||
return ctx;
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import { BuildContext } from '@expo/build-tools';
|
||||
import { EnvironmentSecretType, Job } from '@expo/eas-build-job';
|
||||
import formatBytes from 'filesize';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
|
||||
import config from './config';
|
||||
import { Environment } from './constants';
|
||||
import { ResourceClass } from './external/turtle';
|
||||
|
||||
const RESOURCE_CLASS_DESCRIPTION: Record<ResourceClass, string> = {
|
||||
[ResourceClass.ANDROID_N2_1_3_12]: 'Intel, 4 vCPUs, 16 GB RAM',
|
||||
[ResourceClass.ANDROID_N2_2_6_24]: 'Intel, 8 vCPUs, 32 GB RAM',
|
||||
[ResourceClass.IOS_M1_4_16]: 'M1, 2 cores, 8 GB RAM',
|
||||
[ResourceClass.IOS_M2_2_8]: 'M2, 2 cores, 8 GB RAM',
|
||||
[ResourceClass.IOS_M2_PRO_4_12]: 'M2 Pro, 4 cores, 12 GB RAM',
|
||||
[ResourceClass.IOS_M4_PRO_5_20]: 'M4 Pro, 5 cores, 20 GB RAM',
|
||||
[ResourceClass.IOS_M4_PRO_10_40]: 'M4 Pro, 10 cores, 40 GB RAM',
|
||||
[ResourceClass.IOS_M2_4_22]: 'M2, 4 cores, 22 GB RAM',
|
||||
[ResourceClass.LINUX_C3D_STANDARD_4]: 'AMD, 4 vCPUs, 16 GB RAM',
|
||||
[ResourceClass.LINUX_C3D_STANDARD_8]: 'AMD, 8 vCPUs, 32 GB RAM',
|
||||
[ResourceClass.LINUX_C4D_STANDARD_4]: 'AMD, 4 vCPUs, 15 GB RAM',
|
||||
[ResourceClass.LINUX_C4D_STANDARD_8]: 'AMD, 8 vCPUs, 31 GB RAM',
|
||||
};
|
||||
|
||||
export function displayWorkerRuntimeInfo(ctx: BuildContext<Job>): void {
|
||||
printVMSpecs(ctx);
|
||||
@@ -29,22 +15,29 @@ export function displayWorkerRuntimeInfo(ctx: BuildContext<Job>): void {
|
||||
}
|
||||
|
||||
function printVMSpecs(ctx: BuildContext<Job>): void {
|
||||
const { resourceClass } = config;
|
||||
const specs = [
|
||||
getCpuModelDescription(),
|
||||
`${os.cpus().length} vCPUs`,
|
||||
`${formatBytes(os.totalmem())} RAM`,
|
||||
].flatMap(spec => spec || []);
|
||||
|
||||
if (resourceClass) {
|
||||
const resourceClassDescription = RESOURCE_CLASS_DESCRIPTION[resourceClass];
|
||||
ctx.logger.info(specs.join(', '));
|
||||
ctx.logger.info('');
|
||||
}
|
||||
|
||||
if (resourceClassDescription) {
|
||||
ctx.logger.info(resourceClassDescription);
|
||||
} else {
|
||||
ctx.logger.debug(
|
||||
`Resource class is unsupported: ${resourceClass}, valid values: ${Object.keys(
|
||||
RESOURCE_CLASS_DESCRIPTION
|
||||
)}}`
|
||||
);
|
||||
}
|
||||
ctx.logger.info('');
|
||||
function getCpuModelDescription(): string | null {
|
||||
const cpuModel = os.cpus()[0]?.model.trim().replaceAll(/\s+/g, ' ').replace(' (Virtual)', '');
|
||||
if (!cpuModel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (cpuModel.toLowerCase().includes('intel')) {
|
||||
return 'Intel';
|
||||
} else if (cpuModel.toLowerCase().includes('amd')) {
|
||||
return 'AMD';
|
||||
}
|
||||
|
||||
return cpuModel;
|
||||
}
|
||||
|
||||
function printImageMessage(ctx: BuildContext<Job>): void {
|
||||
|
||||
+10
-25
@@ -1,15 +1,12 @@
|
||||
import { Env, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { spawnSync } from 'child_process';
|
||||
import micromatch from 'micromatch';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import config from './config';
|
||||
import { Environment } from './constants';
|
||||
import {
|
||||
ResourceClass,
|
||||
ResourceClassToPlatform,
|
||||
androidImagesWithJavaVersionLowerThen11,
|
||||
} from './external/turtle';
|
||||
import { androidImagesWithJavaVersionLowerThen11 } from './external/turtle';
|
||||
import { getAccessedEnvs } from './utils/env';
|
||||
|
||||
// keep in sync with local-build-plugin env vars
|
||||
@@ -48,8 +45,7 @@ export function getBuildEnv({
|
||||
setEnv(env, 'EAS_BUILD_WORKINGDIR', path.join(config.workingdir, 'build'));
|
||||
setEnv(env, 'EAS_BUILD_PROJECT_ID', projectId);
|
||||
|
||||
const runnerPlatform =
|
||||
job.platform ?? (config.resourceClass && ResourceClassToPlatform[config.resourceClass]);
|
||||
const runnerPlatform = job.platform;
|
||||
if (runnerPlatform === Platform.IOS) {
|
||||
setEnv(env, 'EAS_BUILD_COCOAPODS_CACHE_URL', config.cocoapodsCacheUrl);
|
||||
setEnv(env, 'COMPILER_INDEX_STORE_ENABLE', 'NO');
|
||||
@@ -77,9 +73,7 @@ export function getBuildEnv({
|
||||
}
|
||||
|
||||
if (config.env !== Environment.TEST) {
|
||||
const maxHeapSize = config.resourceClass
|
||||
? (ResourceClassToMaxHeapSize[config.resourceClass] ?? '4g')
|
||||
: '4g';
|
||||
const { maxHeapSize } = getGradleMemoryOptions();
|
||||
|
||||
setEnv(
|
||||
env,
|
||||
@@ -163,19 +157,10 @@ function setEnv(env: Env, key: string, value: string | null | undefined): void {
|
||||
}
|
||||
}
|
||||
|
||||
const ResourceClassToMaxHeapSize: Record<ResourceClass, string | null> = {
|
||||
[ResourceClass.LINUX_C3D_STANDARD_4]: '4g', // 4 out of 16 GB, 12 GB left
|
||||
[ResourceClass.LINUX_C3D_STANDARD_8]: '8g', // 8 out of 32 GB, 24 GB left
|
||||
[ResourceClass.LINUX_C4D_STANDARD_4]: '4g', // 4 out of 15 GB, 11 GB left
|
||||
[ResourceClass.LINUX_C4D_STANDARD_8]: '8g', // 8 out of 31 GB, 23 GB left
|
||||
[ResourceClass.ANDROID_N2_1_3_12]: '4g', // 4 out of 12 GB, 8 GB left
|
||||
[ResourceClass.ANDROID_N2_2_6_24]: '8g', // 8 out of 24 GB, 16 GB left
|
||||
export function getGradleMemoryOptions() {
|
||||
const totalMemoryGb = os.totalmem() / 1024 / 1024 / 1024;
|
||||
|
||||
// We don't care about Gradle heap size for macOS resource classes
|
||||
[ResourceClass.IOS_M1_4_16]: null,
|
||||
[ResourceClass.IOS_M2_2_8]: null,
|
||||
[ResourceClass.IOS_M2_PRO_4_12]: null,
|
||||
[ResourceClass.IOS_M4_PRO_5_20]: null,
|
||||
[ResourceClass.IOS_M4_PRO_10_40]: null,
|
||||
[ResourceClass.IOS_M2_4_22]: null,
|
||||
};
|
||||
return {
|
||||
maxHeapSize: `${Math.max(1, Math.round(totalMemoryGb / 4))}g`,
|
||||
};
|
||||
}
|
||||
-1
@@ -82,7 +82,6 @@ export async function logProjectDependenciesAsync(
|
||||
});
|
||||
} catch (error: any) {
|
||||
const msg = 'Failed to report project dependencies metrics';
|
||||
ctx.logger.error({ err: error }, msg);
|
||||
sentry.capture(msg, error, {
|
||||
level: 'warning',
|
||||
tags: {
|
||||
|
||||
+2
-49
@@ -1,43 +1,5 @@
|
||||
import { GCS } from '@expo/build-tools';
|
||||
import {
|
||||
BuildJob,
|
||||
BuildPhase,
|
||||
BuildPhaseResult,
|
||||
Generic,
|
||||
Metadata,
|
||||
Platform,
|
||||
errors,
|
||||
} from '@expo/eas-build-job';
|
||||
|
||||
export enum ResourceClass {
|
||||
ANDROID_N2_1_3_12 = 'android-n2-1.3-12',
|
||||
ANDROID_N2_2_6_24 = 'android-n2-2.6-24',
|
||||
IOS_M1_4_16 = 'ios-m1-4-16',
|
||||
IOS_M2_2_8 = 'ios-m2-2-8',
|
||||
IOS_M2_PRO_4_12 = 'ios-m2-pro-4-12',
|
||||
IOS_M2_4_22 = 'ios-m2-4-22',
|
||||
IOS_M4_PRO_5_20 = 'ios-m4-pro-5-20',
|
||||
IOS_M4_PRO_10_40 = 'ios-m4-pro-10-40',
|
||||
LINUX_C3D_STANDARD_4 = 'linux-c3d-standard-4',
|
||||
LINUX_C3D_STANDARD_8 = 'linux-c3d-standard-8',
|
||||
LINUX_C4D_STANDARD_4 = 'linux-c4d-standard-4',
|
||||
LINUX_C4D_STANDARD_8 = 'linux-c4d-standard-8',
|
||||
}
|
||||
|
||||
export const ResourceClassToPlatform: Record<ResourceClass, Platform> = {
|
||||
'android-n2-1.3-12': Platform.ANDROID,
|
||||
'android-n2-2.6-24': Platform.ANDROID,
|
||||
'ios-m1-4-16': Platform.IOS,
|
||||
'ios-m2-2-8': Platform.IOS,
|
||||
'ios-m2-4-22': Platform.IOS,
|
||||
'ios-m2-pro-4-12': Platform.IOS,
|
||||
'ios-m4-pro-5-20': Platform.IOS,
|
||||
'ios-m4-pro-10-40': Platform.IOS,
|
||||
[ResourceClass.LINUX_C3D_STANDARD_4]: Platform.ANDROID,
|
||||
[ResourceClass.LINUX_C3D_STANDARD_8]: Platform.ANDROID,
|
||||
[ResourceClass.LINUX_C4D_STANDARD_4]: Platform.ANDROID,
|
||||
[ResourceClass.LINUX_C4D_STANDARD_8]: Platform.ANDROID,
|
||||
};
|
||||
import { BuildJob, Generic, Metadata, errors } from '@expo/eas-build-job';
|
||||
|
||||
export const androidImagesWithJavaVersionLowerThen11 = [
|
||||
'ubuntu-20.04-jdk-8-ndk-r19c',
|
||||
@@ -67,7 +29,6 @@ export namespace Worker {
|
||||
mavenCacheUrl: string | undefined;
|
||||
cocoapodsCacheUrl: string | undefined;
|
||||
runMetricsServer: boolean;
|
||||
resourceClass: ResourceClass;
|
||||
|
||||
type: 'jobRun';
|
||||
buildId: string;
|
||||
@@ -86,7 +47,6 @@ export namespace Worker {
|
||||
mavenCacheUrl: string | undefined;
|
||||
cocoapodsCacheUrl: string | undefined;
|
||||
runMetricsServer: boolean;
|
||||
resourceClass: ResourceClass;
|
||||
|
||||
type?: never;
|
||||
buildId: string;
|
||||
@@ -100,7 +60,6 @@ export namespace WorkerMessage {
|
||||
STATE_RESPONSE = 'state-response',
|
||||
SUCCESS = 'success',
|
||||
ERROR = 'error',
|
||||
BUILD_PHASE_STATS = 'build-phase-stats',
|
||||
ABORTED = 'aborted',
|
||||
}
|
||||
|
||||
@@ -109,7 +68,7 @@ export namespace WorkerMessage {
|
||||
TIMEOUT = 'timeout',
|
||||
}
|
||||
|
||||
export type Message = StateResponse | BuildSuccess | BuildError | BuildPhaseStats | BuildAborted;
|
||||
export type Message = StateResponse | BuildSuccess | BuildError | BuildAborted;
|
||||
export interface StateResponse {
|
||||
type: MessageType.STATE_RESPONSE;
|
||||
status: Worker.Status;
|
||||
@@ -131,12 +90,6 @@ export namespace WorkerMessage {
|
||||
applicationArchiveName: string | null;
|
||||
buildArtifactsName: string | null;
|
||||
}
|
||||
export interface BuildPhaseStats {
|
||||
type: MessageType.BUILD_PHASE_STATS;
|
||||
buildPhase: BuildPhase;
|
||||
result: BuildPhaseResult;
|
||||
durationMs: number;
|
||||
}
|
||||
export interface BuildAborted {
|
||||
type: MessageType.ABORTED;
|
||||
reason?: AbortReason;
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
BuildJob,
|
||||
BuildMode,
|
||||
BuildPhase,
|
||||
BuildPhaseStats,
|
||||
Ios,
|
||||
Job,
|
||||
Metadata,
|
||||
@@ -241,16 +240,6 @@ export default class BuildService {
|
||||
});
|
||||
}
|
||||
|
||||
public reportBuildPhaseStats(stats: BuildPhaseStats): void {
|
||||
if (this.ws) {
|
||||
logger.info(stats, 'Sending build phase stats');
|
||||
this.ws.send({
|
||||
type: WorkerMessage.MessageType.BUILD_PHASE_STATS,
|
||||
...stats,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public syncLauncherState({ buildId }: LauncherMessage.StateQuery): void {
|
||||
if (this.buildId !== buildId) {
|
||||
// should not happen (malicious or invalid connection)
|
||||
@@ -307,9 +296,6 @@ export default class BuildService {
|
||||
projectId,
|
||||
buildId: this.buildId,
|
||||
buildLogger,
|
||||
reportBuildPhaseStatsFn: stats => {
|
||||
this.reportBuildPhaseStats(stats);
|
||||
},
|
||||
});
|
||||
this.buildContext = ctx;
|
||||
|
||||
|
||||
@@ -2856,14 +2856,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/build-tools@npm:19.0.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
"@expo/build-tools@npm:19.1.0, @expo/build-tools@workspace:packages/build-tools":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/build-tools@workspace:packages/build-tools"
|
||||
dependencies:
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/downloader": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@expo/env": "npm:^0.4.0"
|
||||
"@expo/logger": "npm:19.0.0"
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
@@ -2871,7 +2871,7 @@ __metadata:
|
||||
"@expo/repack-app": "npm:~0.4.1"
|
||||
"@expo/results": "npm:^1.0.0"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:19.0.0"
|
||||
"@expo/steps": "npm:19.1.0"
|
||||
"@expo/template-file": "npm:19.0.0"
|
||||
"@expo/turtle-spawn": "npm:19.0.0"
|
||||
"@expo/xcpretty": "npm:^4.3.1"
|
||||
@@ -3112,7 +3112,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-build-job@npm:19.0.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
"@expo/eas-build-job@npm:19.1.0, @expo/eas-build-job@workspace:packages/eas-build-job":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-build-job@workspace:packages/eas-build-job"
|
||||
dependencies:
|
||||
@@ -3131,12 +3131,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@expo/eas-json@npm:19.0.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
"@expo/eas-json@npm:19.1.0, @expo/eas-json@workspace:packages/eas-json":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/eas-json@workspace:packages/eas-json"
|
||||
dependencies:
|
||||
"@babel/code-frame": "npm:7.23.5"
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@tsconfig/node20": "npm:20.1.8"
|
||||
"@types/babel__code-frame": "npm:7.0.3"
|
||||
"@types/fs-extra": "npm:11.0.4"
|
||||
@@ -3484,11 +3484,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/steps@npm:19.0.0, @expo/steps@workspace:packages/steps":
|
||||
"@expo/steps@npm:19.1.0, @expo/steps@workspace:packages/steps":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@expo/steps@workspace:packages/steps"
|
||||
dependencies:
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@expo/logger": "npm:19.0.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
@@ -3557,16 +3557,16 @@ __metadata:
|
||||
resolution: "@expo/worker@workspace:packages/worker"
|
||||
dependencies:
|
||||
"@babel/core": "npm:7.24.7"
|
||||
"@expo/build-tools": "npm:19.0.0"
|
||||
"@expo/build-tools": "npm:19.1.0"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/config-types": "npm:55.0.5"
|
||||
"@expo/downloader": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@expo/logger": "npm:19.0.0"
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:19.0.0"
|
||||
"@expo/steps": "npm:19.1.0"
|
||||
"@expo/template-file": "npm:19.0.0"
|
||||
"@expo/turtle-spawn": "npm:19.0.0"
|
||||
"@hapi/boom": "npm:10.0.1"
|
||||
@@ -10348,7 +10348,7 @@ __metadata:
|
||||
resolution: "create-eas-build-function@workspace:packages/create-eas-build-function"
|
||||
dependencies:
|
||||
"@expo/package-manager": "npm:1.9.10"
|
||||
"@expo/steps": "npm:19.0.0"
|
||||
"@expo/steps": "npm:19.1.0"
|
||||
"@jest/globals": "npm:^29.7.0"
|
||||
"@types/jest": "npm:^29.5.12"
|
||||
"@types/node": "npm:20.14.2"
|
||||
@@ -10961,8 +10961,8 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli-local-build-plugin@workspace:packages/local-build-plugin"
|
||||
dependencies:
|
||||
"@expo/build-tools": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/build-tools": "npm:19.1.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@expo/logger": "npm:19.0.0"
|
||||
"@expo/spawn-async": "npm:^1.7.2"
|
||||
"@expo/turtle-spawn": "npm:19.0.0"
|
||||
@@ -11019,8 +11019,8 @@ __metadata:
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
"@expo/eas-build-job": "npm:19.0.0"
|
||||
"@expo/eas-json": "npm:19.0.0"
|
||||
"@expo/eas-build-job": "npm:19.1.0"
|
||||
"@expo/eas-json": "npm:19.1.0"
|
||||
"@expo/env": "npm:^1.0.0"
|
||||
"@expo/json-file": "npm:8.3.3"
|
||||
"@expo/logger": "npm:19.0.0"
|
||||
@@ -11035,7 +11035,7 @@ __metadata:
|
||||
"@expo/results": "npm:1.0.0"
|
||||
"@expo/rudder-sdk-node": "npm:1.1.1"
|
||||
"@expo/spawn-async": "npm:1.7.2"
|
||||
"@expo/steps": "npm:19.0.0"
|
||||
"@expo/steps": "npm:19.1.0"
|
||||
"@expo/timeago.js": "npm:1.0.0"
|
||||
"@graphql-codegen/cli": "npm:5.0.0"
|
||||
"@graphql-codegen/introspection": "npm:4.0.0"
|
||||
|
||||
Reference in new issue
Block a user