@expo/build-tools 21.6.0 → 21.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ const node_fetch_1 = __importDefault(require("node-fetch"));
8
8
  const stream_1 = require("stream");
9
9
  const retry_1 = require("../utils/retry");
10
10
  const MAX_BATCH_BYTES = 200_000;
11
+ const REQUEST_TIMEOUT_MS = 10_000;
11
12
  /**
12
13
  * A bunyan-compatible writable stream for sending logs over HTTP.
13
14
  */
@@ -120,6 +121,7 @@ class HttpLogStream extends stream_1.Writable {
120
121
  'Content-Type': 'application/x-ndjson',
121
122
  },
122
123
  body: logs.map(log => log.serialized).join('\n'),
124
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
123
125
  });
124
126
  if (!response.ok) {
125
127
  const responseText = await (0, results_1.asyncResult)(response.text());
@@ -37,6 +37,11 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
37
37
  required: false,
38
38
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
39
39
  }),
40
+ steps_1.BuildStepInput.createProvider({
41
+ id: 'max_idle_time_minutes',
42
+ required: false,
43
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
44
+ }),
40
45
  ],
41
46
  fn: async ({ logger, global }, { inputs, env, signal }) => {
42
47
  // Fail fast before any expensive setup if the injected env
@@ -47,6 +52,8 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
47
52
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
48
53
  const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
49
54
  const packageVersion = inputs.package_version.value;
55
+ // A missing or non-positive value disables the idle timeout (opt-in feature).
56
+ const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
50
57
  const { runtimePlatform } = global;
51
58
  logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
52
59
  if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
@@ -109,6 +116,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
109
116
  deviceRunSessionId,
110
117
  logger,
111
118
  signal,
119
+ idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
120
+ ? {
121
+ maxIdleTimeMinutes,
122
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
123
+ }
124
+ : undefined,
112
125
  });
113
126
  }
114
127
  finally {
@@ -52,6 +52,11 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
52
52
  required: false,
53
53
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
54
54
  }),
55
+ steps_1.BuildStepInput.createProvider({
56
+ id: 'max_idle_time_minutes',
57
+ required: false,
58
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
59
+ }),
55
60
  ],
56
61
  fn: async ({ logger, global }, { inputs, env, signal }) => {
57
62
  // Fail fast before any expensive setup if the injected env
@@ -62,6 +67,8 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
62
67
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
63
68
  const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
64
69
  const packageVersion = inputs.package_version.value;
70
+ // A missing or non-positive value disables the idle timeout (opt-in feature).
71
+ const maxIdleTimeMinutes = inputs.max_idle_time_minutes.value;
65
72
  warnIfArgentPackageVersionCannotBeVerified({ packageVersion, logger });
66
73
  const versionSpec = packageVersion ?? 'latest';
67
74
  const { runtimePlatform } = global;
@@ -172,6 +179,12 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
172
179
  deviceRunSessionId,
173
180
  logger,
174
181
  signal,
182
+ idleTimeout: maxIdleTimeMinutes !== undefined && maxIdleTimeMinutes > 0
183
+ ? {
184
+ maxIdleTimeMinutes,
185
+ getLastEventObservedAt: eventCollection.getLastEventObservedAt,
186
+ }
187
+ : undefined,
175
188
  });
176
189
  }
177
190
  finally {
@@ -1,11 +1,10 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { type CustomBuildContext } from '../../customBuildContext';
3
+ import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
3
4
  export declare function startAgentDeviceEventCollectionAsync({ ctx, deviceRunSessionId, stateDir, logger, pollIntervalMs, }: {
4
5
  ctx: CustomBuildContext;
5
6
  deviceRunSessionId: string;
6
7
  stateDir: string;
7
8
  logger: bunyan;
8
9
  pollIntervalMs?: number;
9
- }): Promise<{
10
- stopAsync: () => Promise<void>;
11
- }>;
10
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -21,6 +21,17 @@ const deviceRunSessionArtifacts_1 = require("./deviceRunSessionArtifacts");
21
21
  const ARGENT_ARTIFACT_UPLOAD_POLL_INTERVAL_MS = 5_000;
22
22
  const ARGENT_ARTIFACT_UPLOAD_CLEANUP_TIMEOUT_MS = 30_000;
23
23
  const ARGENT_ARTIFACT_FETCH_TIMEOUT_MS = 10_000;
24
+ // Kinds the EAS dashboard groups device run session artifacts by. Only media types the dashboard
25
+ // renders specially are mapped; anything else stays unclassified and lands in its "Other" group.
26
+ // Note that these kinds only affect grouping and labelling. The dashboard gates its inline video
27
+ // player on the `__eas_screen_recording` metadata flag, which Argent artifacts deliberately do not
28
+ // set, so a `screen-recording` kind here does not add a player to the session page.
29
+ const ARGENT_ARTIFACT_KIND_BY_MIME_TYPE = new Map([
30
+ ['image/png', 'screenshot'],
31
+ ['image/jpeg', 'screenshot'],
32
+ ['video/mp4', 'screen-recording'],
33
+ ['video/quicktime', 'screen-recording'],
34
+ ]);
24
35
  const ArgentArtifactSchema = zod_1.z.object({
25
36
  id: zod_1.z.string(),
26
37
  filename: zod_1.z.string(),
@@ -30,6 +41,16 @@ const ArgentArtifactSchema = zod_1.z.object({
30
41
  const ArgentArtifactsListResponseSchema = zod_1.z.object({
31
42
  artifacts: zod_1.z.array(ArgentArtifactSchema),
32
43
  });
44
+ function getArgentArtifactKind(artifact) {
45
+ // Directories are repackaged as a tarball before upload, so the reported media type describes the
46
+ // contents rather than the file we actually store.
47
+ if (artifact.isDirectory) {
48
+ return undefined;
49
+ }
50
+ // Media types are case-insensitive and may carry parameters, e.g. `image/png; charset=binary`.
51
+ const mediaType = artifact.mimeType.split(';')[0].trim().toLowerCase();
52
+ return ARGENT_ARTIFACT_KIND_BY_MIME_TYPE.get(mediaType);
53
+ }
33
54
  async function pollArgentArtifactsForUploadAsync(ctx, { deviceRunSessionId, toolsUrl, toolsAuthToken, logger, signal, }) {
34
55
  logger.info('Started polling Argent tool-server for artifacts.');
35
56
  const seenArtifactIds = new Set();
@@ -126,7 +147,7 @@ async function uploadArgentArtifactAsync(ctx, { deviceRunSessionId, toolsUrl, to
126
147
  artifactId: artifact.id,
127
148
  name: `${filename} (${artifact.id})`,
128
149
  filename,
129
- kind: undefined,
150
+ kind: getArgentArtifactKind(artifact),
130
151
  size,
131
152
  stream: (0, node_fs_1.createReadStream)(temporaryArtifactPath),
132
153
  });
@@ -1,5 +1,6 @@
1
1
  import { type bunyan } from '@expo/logger';
2
2
  import { type CustomBuildContext } from '../../customBuildContext';
3
+ import { type DeviceRunSessionEventCollection } from './deviceRunSessionEvents';
3
4
  export declare const ARGENT_EVENT_LOG_FILENAME = "tool-server-events.jsonl";
4
5
  export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionId, eventLogPath, logger, pollIntervalMs, }: {
5
6
  ctx: CustomBuildContext;
@@ -7,6 +8,4 @@ export declare function startArgentEventCollectionAsync({ ctx, deviceRunSessionI
7
8
  eventLogPath: string;
8
9
  logger: bunyan;
9
10
  pollIntervalMs?: number;
10
- }): Promise<{
11
- stopAsync: () => Promise<void>;
12
- }>;
11
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -53,12 +53,20 @@ export type DeviceRunSessionEventSource = {
53
53
  deviceRunSessionId: string;
54
54
  }) => DeviceRunSessionEventParseResult;
55
55
  };
56
+ export type DeviceRunSessionEventCollection = {
57
+ stopAsync: () => Promise<void>;
58
+ /**
59
+ * Local arrival time of the most recently collected event, or `undefined`
60
+ * when no event has been observed yet. Uses the collector's clock rather
61
+ * than the event's embedded timestamp so producer clock skew cannot affect
62
+ * idle detection.
63
+ */
64
+ getLastEventObservedAt: () => Date | undefined;
65
+ };
56
66
  export declare function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSessionId, source, logger, pollIntervalMs, }: {
57
67
  ctx: CustomBuildContext;
58
68
  deviceRunSessionId: string;
59
69
  source: DeviceRunSessionEventSource;
60
70
  logger: bunyan;
61
71
  pollIntervalMs?: number;
62
- }): Promise<{
63
- stopAsync: () => Promise<void>;
64
- }>;
72
+ }): Promise<DeviceRunSessionEventCollection>;
@@ -58,7 +58,13 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
58
58
  const error = err instanceof Error ? err : new Error(String(err));
59
59
  logger.warn({ err: error }, 'Could not persist device run session events to the artifact.');
60
60
  reportEventLogFailure(error, 'setup');
61
- return { stopAsync: async () => { } };
61
+ return {
62
+ stopAsync: async () => { },
63
+ // Collection is disabled, so session activity is invisible here. Report
64
+ // fresh activity so an enabled idle timeout never stops a session it
65
+ // cannot observe.
66
+ getLastEventObservedAt: () => new Date(),
67
+ };
62
68
  }
63
69
  let didReportRealtimeLogFailure = false;
64
70
  const reportRealtimeLogFailure = (error, operation) => {
@@ -83,6 +89,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
83
89
  }
84
90
  const states = new Map();
85
91
  const controller = new AbortController();
92
+ let lastEventObservedAt;
86
93
  let parseFailureCount = 0;
87
94
  const parseFailureCounts = {
88
95
  'invalid-json': 0,
@@ -106,6 +113,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
106
113
  source,
107
114
  deviceRunSessionId,
108
115
  writeEvent: event => {
116
+ lastEventObservedAt = new Date();
109
117
  eventLogStream.write(event);
110
118
  realtimeLogStream?.write({ ...event, logId: event.eventId });
111
119
  },
@@ -170,6 +178,7 @@ async function startDeviceRunSessionEventCollectionAsync({ ctx, deviceRunSession
170
178
  // A diagnostics failure must not recreate an unhandled rejection in this best-effort poller.
171
179
  });
172
180
  return {
181
+ getLastEventObservedAt: () => lastEventObservedAt,
173
182
  stopAsync: async () => {
174
183
  controller.abort();
175
184
  await pollingPromise;
@@ -211,7 +220,7 @@ function createRealtimeLogStream(ctx, logger) {
211
220
  ? 'http://localhost:4999/logs/'
212
221
  : ctx.env.EXPO_STAGING
213
222
  ? 'https://staging-logs.expo.dev/logs/'
214
- : undefined;
223
+ : 'https://logs.expo.dev/logs/';
215
224
  const jobRunId = ctx.env.EAS_BUILD_ID;
216
225
  const robotAccessToken = ctx.job.secrets?.robotAccessToken;
217
226
  if (!baseUrl || !jobRunId || !robotAccessToken) {
@@ -15,11 +15,22 @@ export declare function selectXcodeDeveloperDirectoryAsync({ env, logger, }: {
15
15
  env: BuildStepEnv;
16
16
  logger: bunyan;
17
17
  }): Promise<void>;
18
- export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }: {
18
+ export type DeviceRunSessionIdleTimeout = {
19
+ /** Stop the session after this many minutes without observed activity. */
20
+ maxIdleTimeMinutes: number;
21
+ /**
22
+ * Local arrival time of the most recent session event, or `undefined` when
23
+ * no event has been observed yet. The idle clock starts when the wait
24
+ * begins, so a session nobody ever connects to still times out.
25
+ */
26
+ getLastEventObservedAt: () => Date | undefined;
27
+ };
28
+ export declare function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }: {
19
29
  ctx: CustomBuildContext;
20
30
  deviceRunSessionId: string;
21
31
  logger: bunyan;
22
32
  signal?: AbortSignal;
33
+ idleTimeout?: DeviceRunSessionIdleTimeout;
23
34
  }): Promise<void>;
24
35
  /**
25
36
  * Install ffmpeg when the runtime does not already provide it, so argent's
@@ -97,6 +97,16 @@ const DEVICE_RUN_SESSION_STATUS_QUERY = (0, gql_tada_1.graphql)(`
97
97
  }
98
98
  }
99
99
  `);
100
+ const ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION = (0, gql_tada_1.graphql)(`
101
+ mutation EnsureDeviceRunSessionStopped($deviceRunSessionId: ID!) {
102
+ deviceRunSession {
103
+ ensureDeviceRunSessionStopped(deviceRunSessionId: $deviceRunSessionId) {
104
+ id
105
+ status
106
+ }
107
+ }
108
+ }
109
+ `);
100
110
  const DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS = 5_000;
101
111
  function getDeviceRunSessionIdOrThrow(env) {
102
112
  const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
@@ -142,10 +152,26 @@ async function selectXcodeDeveloperDirectoryAsync({ env, logger, }) {
142
152
  stdio: ['ignore', 'pipe', 'pipe'],
143
153
  });
144
154
  }
145
- async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, }) {
155
+ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, logger, signal, idleTimeout, }) {
146
156
  logger.info(`Remote session is live. Polling device run session ${deviceRunSessionId} until it is stopped.`);
157
+ if (idleTimeout) {
158
+ logger.info(`The session stops automatically after ${idleTimeout.maxIdleTimeMinutes} minute(s) without activity.`);
159
+ }
147
160
  let pollErrorCount = 0;
161
+ let lastActivityAt = new Date();
148
162
  while (!signal?.aborted) {
163
+ if (idleTimeout) {
164
+ const lastEventObservedAt = idleTimeout.getLastEventObservedAt();
165
+ if (lastEventObservedAt && lastEventObservedAt > lastActivityAt) {
166
+ lastActivityAt = lastEventObservedAt;
167
+ }
168
+ if (Date.now() - lastActivityAt.getTime() >= idleTimeout.maxIdleTimeMinutes * 60_000) {
169
+ logger.info(`Device run session ${deviceRunSessionId} had no activity for ` +
170
+ `${idleTimeout.maxIdleTimeMinutes} minute(s) (max idle time). Stopping the session.`);
171
+ await ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger });
172
+ return;
173
+ }
174
+ }
149
175
  try {
150
176
  const result = await ctx.graphqlClient
151
177
  .query(DEVICE_RUN_SESSION_STATUS_QUERY, { deviceRunSessionId })
@@ -180,6 +206,26 @@ async function waitForDeviceRunSessionStoppedAsync({ ctx, deviceRunSessionId, lo
180
206
  await sleepUntilAbortedAsync(DEVICE_RUN_SESSION_STATUS_POLL_INTERVAL_MS, signal);
181
207
  }
182
208
  }
209
+ // Best effort: when this fails, the caller still tears the session down and the
210
+ // job run finishes, which clients also treat as the session ending.
211
+ async function ensureDeviceRunSessionStoppedSafelyAsync({ ctx, deviceRunSessionId, logger, }) {
212
+ try {
213
+ const result = await ctx.graphqlClient
214
+ .mutation(ENSURE_DEVICE_RUN_SESSION_STOPPED_MUTATION, { deviceRunSessionId })
215
+ .toPromise();
216
+ if (result.error) {
217
+ throw result.error;
218
+ }
219
+ }
220
+ catch (err) {
221
+ const error = err instanceof Error ? err : new Error(String(err));
222
+ sentry_1.Sentry.capture('Could not mark idle device run session as stopped', error, {
223
+ level: 'warning',
224
+ extras: { deviceRunSessionId },
225
+ });
226
+ logger.warn({ err: error }, `Could not mark device run session ${deviceRunSessionId} as stopped. The session job ends anyway.`);
227
+ }
228
+ }
183
229
  async function sleepUntilAbortedAsync(timeoutMs, signal) {
184
230
  try {
185
231
  await (0, promises_1.setTimeout)(timeoutMs, undefined, signal ? { signal } : undefined);
@@ -20,17 +20,19 @@ async function uploadServeSimMetricsFileAsync(ctx, { deviceRunSessionId, udid, f
20
20
  logger.info(`serve-sim metrics file for ${udid} is empty; skipping upload.`);
21
21
  return;
22
22
  }
23
+ const deviceName = typeof metadata?.deviceName === 'string' ? metadata.deviceName : undefined;
23
24
  try {
24
- logger.info(`Uploading serve-sim metrics for ${udid} (${(0, artifacts_1.formatBytes)(size)}).`);
25
+ logger.info(`Uploading serve-sim metrics for ${deviceName ?? udid} (${(0, artifacts_1.formatBytes)(size)}).`);
25
26
  await (0, deviceRunSessionArtifacts_1.uploadDeviceRunSessionArtifactAsync)(ctx, {
26
27
  deviceRunSessionId,
27
28
  artifactId: `metrics-${udid}`,
28
- name: `Performance metrics (${udid.slice(0, 8)})`,
29
+ name: `Performance metrics (${deviceName ?? udid.slice(0, 8)})`,
29
30
  filename: METRICS_ARTIFACT_FILENAME,
30
31
  kind: 'performance-metrics',
31
32
  metadata: {
32
33
  __eas_type: 'performance-metrics',
33
34
  udid,
35
+ ...(deviceName && { deviceName }),
34
36
  ...(metadata && {
35
37
  hostCores: metadata.hostCores,
36
38
  sampleIntervalMs: metadata.sampleIntervalMs,
@@ -1 +1 @@
1
- export declare const EasBuildConfigureVersionGradleTemplate = "// Build integration with EAS\n\nimport java.nio.file.Paths\n\ndef versionCodeVal = null\ndef versionNameVal = null\n<% if (VERSION_CODE) { %>\n versionCodeVal = \"<%- VERSION_CODE %>\"\n<% } %>\n<% if (VERSION_NAME) { %>\n versionNameVal = \"<%- VERSION_NAME %>\"\n<% } %>\n\nandroid {\n defaultConfig {\n if (versionCodeVal) {\n versionCode = Integer.parseInt(versionCodeVal)\n }\n if (versionNameVal) {\n versionName = versionNameVal\n }\n }\n applicationVariants.all { variant ->\n variant.outputs.each { output ->\n if (versionCodeVal) {\n output.versionCodeOverride = Integer.parseInt(versionCodeVal)\n }\n if (versionNameVal) {\n output.versionNameOverride = versionNameVal\n }\n }\n }\n}\n";
1
+ export declare const EasBuildConfigureVersionGradleTemplate = "// Build integration with EAS\n\nimport java.nio.file.Paths\n\ndef versionCodeVal = null\ndef versionNameVal = null\n<% if (VERSION_CODE) { %>\n versionCodeVal = \"<%- VERSION_CODE %>\"\n<% } %>\n<% if (VERSION_NAME) { %>\n versionNameVal = \"<%- VERSION_NAME %>\"\n<% } %>\n\nandroid {\n defaultConfig {\n if (versionCodeVal) {\n versionCode = Integer.parseInt(versionCodeVal)\n }\n if (versionNameVal) {\n versionName = versionNameVal\n }\n }\n}\n\n// AGP 9's new DSL removes the applicationVariants API, we set the version on each output through the androidComponents API instead.\ndef easAndroidExtension = project.extensions.getByName('android')\nif (easAndroidExtension.hasProperty('applicationVariants')) {\n easAndroidExtension.applicationVariants.all { variant ->\n variant.outputs.each { output ->\n if (versionCodeVal) {\n output.versionCodeOverride = Integer.parseInt(versionCodeVal)\n }\n if (versionNameVal) {\n output.versionNameOverride = versionNameVal\n }\n }\n }\n} else {\n def easAndroidComponents = project.extensions.getByName('androidComponents')\n easAndroidComponents.onVariants(easAndroidComponents.selector().all()) { variant ->\n variant.outputs.each { output ->\n if (versionCodeVal) {\n output.versionCode.set(Integer.parseInt(versionCodeVal))\n }\n if (versionNameVal) {\n output.versionName.set(versionNameVal)\n }\n }\n }\n}\n";
@@ -23,7 +23,12 @@ android {
23
23
  versionName = versionNameVal
24
24
  }
25
25
  }
26
- applicationVariants.all { variant ->
26
+ }
27
+
28
+ // AGP 9's new DSL removes the applicationVariants API, we set the version on each output through the androidComponents API instead.
29
+ def easAndroidExtension = project.extensions.getByName('android')
30
+ if (easAndroidExtension.hasProperty('applicationVariants')) {
31
+ easAndroidExtension.applicationVariants.all { variant ->
27
32
  variant.outputs.each { output ->
28
33
  if (versionCodeVal) {
29
34
  output.versionCodeOverride = Integer.parseInt(versionCodeVal)
@@ -33,5 +38,17 @@ android {
33
38
  }
34
39
  }
35
40
  }
41
+ } else {
42
+ def easAndroidComponents = project.extensions.getByName('androidComponents')
43
+ easAndroidComponents.onVariants(easAndroidComponents.selector().all()) { variant ->
44
+ variant.outputs.each { output ->
45
+ if (versionCodeVal) {
46
+ output.versionCode.set(Integer.parseInt(versionCodeVal))
47
+ }
48
+ if (versionNameVal) {
49
+ output.versionName.set(versionNameVal)
50
+ }
51
+ }
52
+ }
36
53
  }
37
54
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "21.6.0",
3
+ "version": "21.7.1",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -46,7 +46,7 @@
46
46
  "@expo/plist": "^0.3.5",
47
47
  "@expo/results": "^1.0.0",
48
48
  "@expo/spawn-async": "1.7.2",
49
- "@expo/steps": "21.6.0",
49
+ "@expo/steps": "21.7.1",
50
50
  "@expo/template-file": "21.0.2",
51
51
  "@expo/turtle-spawn": "21.0.0",
52
52
  "@expo/xcpretty": "^4.3.1",
@@ -100,5 +100,5 @@
100
100
  "typescript": "^5.5.4",
101
101
  "uuid": "^9.0.1"
102
102
  },
103
- "gitHead": "866febb1ba2a66e33859ae8a6c67bd845e8ce8a6"
103
+ "gitHead": "60032049ec947177bbcacf8802c22e1d5f24845f"
104
104
  }