expo-release 2.2.0 → 2.3.0

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.
@@ -1,3 +1,3 @@
1
1
  export { ErrorBoundary, type ErrorBoundaryFallbackProps, type ErrorBoundaryProps } from './error-boundary';
2
2
  export { checkForOTAUpdate, type ReloadApp, type UseOTAUpdatesOptions, useOTAUpdates } from './ota';
3
- export { createPushNotifications, type KeyValueStorage, type NotificationListeners, type PushNotifications, type PushNotificationsOptions, usePushNotifications } from './push';
3
+ export { createPushNotifications, type KeyValueStorage, type NotificationListeners, type PushNotifications, type PushNotificationsOptions, type UsePushNotificationsOptions, usePushNotifications } from './push';
@@ -57,7 +57,8 @@ function createPushNotifications(options) {
57
57
  onUnavailable,
58
58
  onError = (message, error) => console.error(message, error),
59
59
  projectId,
60
- storageKey = "@notifications_permission_asked"
60
+ storageKey = "@notifications_permission_asked",
61
+ tokenStorageKey = "@notifications_push_token"
61
62
  } = options;
62
63
  Notifications.setNotificationHandler({
63
64
  handleNotification: async () => options.foregroundBehavior ?? {
@@ -74,13 +75,23 @@ function createPushNotifications(options) {
74
75
  }).catch((error) => onError("Failed to create the notification channel", error));
75
76
  let pendingPrompt = null;
76
77
  const getPermissionStatus = () => Notifications.getPermissionsAsync();
77
- async function hasPrompted() {
78
+ async function read(key) {
78
79
  try {
79
- return await storage.getItem(storageKey) === "true";
80
+ return await storage.getItem(key);
80
81
  } catch {
81
- return false;
82
+ return null;
83
+ }
84
+ }
85
+ async function rememberToken(token) {
86
+ try {
87
+ await storage.setItem(tokenStorageKey, token);
88
+ } catch (error) {
89
+ onError("Failed to remember the push token", error);
82
90
  }
83
91
  }
92
+ async function hasPrompted() {
93
+ return await read(storageKey) === "true";
94
+ }
84
95
  async function requestPermissions() {
85
96
  if (!Device.isDevice) {
86
97
  onUnavailable?.("not-a-device");
@@ -96,30 +107,40 @@ function createPushNotifications(options) {
96
107
  return false;
97
108
  }
98
109
  }
99
- async function getPushToken() {
110
+ async function getPushToken(devicePushToken) {
100
111
  if (!Device.isDevice)
101
112
  return null;
102
113
  try {
103
- const token = await Notifications.getExpoPushTokenAsync(projectId ? { projectId } : undefined);
114
+ const token = await Notifications.getExpoPushTokenAsync(projectId || devicePushToken ? { projectId, devicePushToken } : undefined);
104
115
  return token.data;
105
116
  } catch (error) {
106
117
  onError("Failed to get the Expo push token", error);
107
118
  return null;
108
119
  }
109
120
  }
110
- async function syncToken() {
111
- if (!(await getPermissionStatus()).granted)
112
- return false;
113
- const token = await getPushToken();
114
- if (!token)
115
- return false;
121
+ async function sendToken(token) {
116
122
  try {
117
123
  await registerDevice(token);
118
- return true;
119
124
  } catch (error) {
120
125
  onError("Failed to register the push token", error);
121
126
  return false;
122
127
  }
128
+ await rememberToken(token);
129
+ return true;
130
+ }
131
+ async function syncToken() {
132
+ if (!(await getPermissionStatus()).granted)
133
+ return false;
134
+ const token = await getPushToken();
135
+ return token ? sendToken(token) : false;
136
+ }
137
+ async function refreshToken(devicePushToken) {
138
+ const sent = await read(tokenStorageKey);
139
+ if (!sent || !(await getPermissionStatus()).granted)
140
+ return;
141
+ const token = await getPushToken(devicePushToken);
142
+ if (token && token !== sent)
143
+ await sendToken(token);
123
144
  }
124
145
  async function runPrompt() {
125
146
  if (await hasPrompted())
@@ -157,11 +178,15 @@ function createPushNotifications(options) {
157
178
  await syncToken();
158
179
  return true;
159
180
  },
160
- disable() {
161
- return registerDevice(null);
181
+ async disable() {
182
+ await registerDevice(null);
183
+ await rememberToken("");
162
184
  },
163
- listen({ onReceived, onResponse }) {
185
+ listen({ onReceived, onResponse } = {}) {
186
+ const refresh = (devicePushToken) => refreshToken(devicePushToken).catch((error) => onError("Failed to refresh the push token", error));
187
+ refresh();
164
188
  const subscriptions = [
189
+ Notifications.addPushTokenListener(refresh),
165
190
  onReceived && Notifications.addNotificationReceivedListener(onReceived),
166
191
  onResponse && Notifications.addNotificationResponseReceivedListener(onResponse)
167
192
  ];
@@ -175,17 +200,31 @@ function createPushNotifications(options) {
175
200
  }
176
201
  };
177
202
  }
178
- function usePushNotifications(push, { enabled = false } = {}) {
203
+ function usePushNotifications(push, { enabled = false, persist } = {}) {
179
204
  const [permission, setPermission] = useState(null);
180
205
  const [isLoading, setIsLoading] = useState(false);
206
+ const [optimistic, setOptimistic] = useState(null);
207
+ const preference = optimistic ?? enabled;
181
208
  const refresh = useCallback(async () => {
182
209
  setPermission(await push.getPermissionStatus());
183
210
  }, [push]);
184
211
  useEffect2(() => {
212
+ setOptimistic(null);
185
213
  refresh();
186
214
  if (enabled)
187
215
  push.syncToken();
188
216
  }, [push, enabled, refresh]);
217
+ const save = useCallback(async (next) => {
218
+ if (!persist)
219
+ return;
220
+ setOptimistic(next);
221
+ try {
222
+ await persist(next);
223
+ } catch (error) {
224
+ setOptimistic(null);
225
+ throw error;
226
+ }
227
+ }, [persist]);
189
228
  const withLoading = useCallback((action) => async () => {
190
229
  setIsLoading(true);
191
230
  try {
@@ -197,11 +236,17 @@ function usePushNotifications(push, { enabled = false } = {}) {
197
236
  }, [refresh]);
198
237
  return {
199
238
  permission,
200
- isEnabled: enabled && permission?.granted === true,
239
+ isEnabled: preference && permission?.granted === true,
201
240
  isLoading,
202
241
  prompt: withLoading(push.prompt),
203
- enable: withLoading(push.enable),
204
- disable: withLoading(push.disable),
242
+ enable: withLoading(async () => {
243
+ await save(true);
244
+ return push.enable();
245
+ }),
246
+ disable: withLoading(async () => {
247
+ await save(false);
248
+ await push.disable();
249
+ }),
205
250
  refresh
206
251
  };
207
252
  }
@@ -5,7 +5,7 @@ export interface KeyValueStorage {
5
5
  setItem(key: string, value: string): Promise<void>;
6
6
  }
7
7
  export interface PushNotificationsOptions {
8
- /** Remembers that the one-time prompt ran. */
8
+ /** Remembers that the one-time prompt ran and the last token sent. */
9
9
  storage: KeyValueStorage;
10
10
  /** Saves this device's Expo push token on your backend; `null` removes it. */
11
11
  registerDevice: (token: string | null) => Promise<void>;
@@ -24,6 +24,8 @@ export interface PushNotificationsOptions {
24
24
  /** How notifications show while the app is in the foreground. */
25
25
  foregroundBehavior?: Notifications.NotificationBehavior;
26
26
  storageKey?: string;
27
+ /** Holds the last token passed to `registerDevice`. */
28
+ tokenStorageKey?: string;
27
29
  }
28
30
  export interface NotificationListeners {
29
31
  onReceived?: (notification: Notifications.Notification) => void;
@@ -39,7 +41,7 @@ export declare function createPushNotifications(options: PushNotificationsOption
39
41
  getPermissionStatus: () => Promise<Notifications.NotificationPermissionsStatus>;
40
42
  hasPrompted: () => Promise<boolean>;
41
43
  requestPermissions: () => Promise<boolean>;
42
- getPushToken: () => Promise<string | null>;
44
+ getPushToken: (devicePushToken?: Notifications.DevicePushToken) => Promise<string | null>;
43
45
  syncToken: () => Promise<boolean>;
44
46
  /**
45
47
  * Asks for push once per install (after sign-in, say), then registers the
@@ -50,16 +52,26 @@ export declare function createPushNotifications(options: PushNotificationsOption
50
52
  enable(): Promise<boolean>;
51
53
  /** Removes this device's token from your backend. */
52
54
  disable(): Promise<void>;
53
- /** Subscribes to notifications. Returns the unsubscribe function. */
54
- listen({ onReceived, onResponse }: NotificationListeners): () => void;
55
+ /**
56
+ * Subscribes to notifications and keeps the registered token current: on
57
+ * start and whenever Expo rotates it. Returns the unsubscribe function.
58
+ */
59
+ listen({ onReceived, onResponse }?: NotificationListeners): () => void;
55
60
  };
61
+ export interface UsePushNotificationsOptions {
62
+ /** The user's saved preference, for example from your API. */
63
+ enabled?: boolean;
64
+ /**
65
+ * Saves the preference. `enable()` and `disable()` call it first and show
66
+ * the new value at once; when it rejects, they roll back and rethrow.
67
+ */
68
+ persist?: (enabled: boolean) => Promise<unknown>;
69
+ }
56
70
  /**
57
71
  * Push state for a settings screen. `enabled` is the user's saved preference;
58
72
  * while it is on, the token is re-registered on mount.
59
73
  */
60
- export declare function usePushNotifications(push: PushNotifications, { enabled }?: {
61
- enabled?: boolean;
62
- }): {
74
+ export declare function usePushNotifications(push: PushNotifications, { enabled, persist }?: UsePushNotificationsOptions): {
63
75
  permission: Notifications.NotificationPermissionsStatus | null;
64
76
  isEnabled: boolean;
65
77
  isLoading: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-release",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "Fingerprint-driven Expo EAS release: OTA update or native store build, plus app runtime helpers",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",
@@ -57,6 +57,10 @@
57
57
  "typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.runtime.json",
58
58
  "verify:package": "node scripts/verify-package.mjs"
59
59
  },
60
+ "dependencies": {
61
+ "hanzio": "^3.1.0",
62
+ "zod": "^4.3.6"
63
+ },
60
64
  "peerDependencies": {
61
65
  "expo-device": ">=55",
62
66
  "expo-notifications": ">=55",
package/src/args.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { parseArgs } from 'node:util'
1
+ import type { CommandContext } from 'hanzio/cli'
2
+ import { z } from 'zod'
2
3
 
3
4
  export const DEFAULT_IMPACT_PATHS = ['apps/expo/'] as const
4
5
 
@@ -39,76 +40,133 @@ export type ReleaseOptions = {
39
40
  dryRun: boolean
40
41
  }
41
42
 
42
- export const USAGE =
43
- 'Usage: expo-release <release|decide|operate> --profile <profile> [--base <sha>] [--head <sha>] [--platform ios|android] [--impact-paths apps/expo/] [--operation deploy|force-native|retry-ota|republish|rollback-embedded] [--deploy-type none|ota|native] [--channel <channel>] [--environment <env>] [--group <id>] [--runtime-version <hash>] [--message <msg>] [--fingerprint <hash>] [--auto-submit true] [--export-source-maps true] [--pre-update <cmd>] [--post-update <cmd>] [--update-extra-args <args>] [--dry-run] [--ignore-expo-impact true]'
44
-
45
- function fromEnv(env: NodeJS.ProcessEnv, name: string): string | undefined {
46
- const value = env[name]?.trim()
47
- return value ? value : undefined
48
- }
49
-
50
- function firstDefined(
51
- ...values: Array<string | undefined>
52
- ): string | undefined {
53
- return values.find((value) => value !== undefined && value.length > 0)
54
- }
55
-
56
- export function optionalString(value: string | undefined): string | undefined {
57
- const trimmed = value?.trim()
58
- return trimmed ? trimmed : undefined
59
- }
60
-
61
- export function parseImpactPaths(value: string | undefined): string[] {
62
- const paths = (value ?? DEFAULT_IMPACT_PATHS.join(','))
63
- .split(',')
64
- .map((path) => path.trim())
65
- .filter(Boolean)
66
- if (paths.length === 0) {
67
- throw new Error('At least one --impact-paths entry is required')
68
- }
69
- return paths
70
- }
71
-
72
- export function parsePlatform(value: string | undefined): ReleasePlatform {
73
- if (value === undefined || value === 'ios') return 'ios'
74
- if (value === 'android') return 'android'
75
- throw new Error('Usage: --platform ios|android')
76
- }
77
-
78
- export function parseBoolean(value: string | undefined, fallback: boolean) {
79
- if (value === undefined) return fallback
80
- if (value === 'true') return true
81
- if (value === 'false') return false
82
- throw new Error(`Expected true or false, received: ${value}`)
83
- }
84
-
85
- export function parseOperation(value: string | undefined): Operation {
86
- if (
87
- value === undefined ||
88
- value === 'deploy' ||
89
- value === 'force-native' ||
90
- value === 'retry-ota' ||
91
- value === 'republish' ||
92
- value === 'rollback-embedded'
93
- ) {
94
- return value ?? 'deploy'
95
- }
96
- throw new Error(
97
- 'Usage: --operation deploy|force-native|retry-ota|republish|rollback-embedded'
98
- )
99
- }
100
-
101
- export function parseDeployType(value: string | undefined): DeployType {
102
- if (value === undefined || value === 'none') return 'none'
103
- if (value === 'ota' || value === 'native') return value
104
- throw new Error('Usage: --deploy-type none|ota|native')
105
- }
106
-
107
- export function parseCommandName(value: string | undefined): CommandName {
108
- if (value === 'release' || value === 'decide' || value === 'operate') {
109
- return value
43
+ // Every flag falls back to an EXPO_RELEASE_* variable: the composite action passes its inputs that way.
44
+ const text = (env: string) =>
45
+ z
46
+ .string()
47
+ .transform((value) => value.trim() || undefined)
48
+ .optional()
49
+ .meta({ env: `EXPO_RELEASE_${env}` })
50
+
51
+ // `--auto-submit false` takes a value, so these are not boolean switches.
52
+ const trueFalse = (env: string, fallback: 'true' | 'false') =>
53
+ z
54
+ .enum(['true', 'false'])
55
+ .default(fallback)
56
+ .transform((value) => value === 'true')
57
+ .meta({ env: `EXPO_RELEASE_${env}` })
58
+
59
+ export const releaseFlags = z
60
+ .object({
61
+ profile: z
62
+ .string()
63
+ .trim()
64
+ .min(1)
65
+ .meta({ env: 'EXPO_RELEASE_PROFILE' })
66
+ .describe('EAS build profile, also the default update channel'),
67
+ base: text('BASE').describe(
68
+ 'Git base SHA for the impact diff (falls back to GITHUB_EVENT_BEFORE)'
69
+ ),
70
+ head: text('HEAD').describe(
71
+ 'Git head SHA for the impact diff (falls back to GITHUB_SHA)'
72
+ ),
73
+ platform: z
74
+ .enum(['ios', 'android'])
75
+ .default('ios')
76
+ .meta({ env: 'EXPO_RELEASE_PLATFORM' })
77
+ .describe('Native platform'),
78
+ impactPaths: z
79
+ .string()
80
+ .default(DEFAULT_IMPACT_PATHS.join(','))
81
+ .transform((value, ctx) => {
82
+ const paths = value
83
+ .split(',')
84
+ .map((path) => path.trim())
85
+ .filter(Boolean)
86
+ if (paths.length === 0)
87
+ ctx.addIssue({
88
+ code: 'custom',
89
+ message: 'At least one --impact-paths entry is required'
90
+ })
91
+ return paths
92
+ })
93
+ .meta({ env: 'EXPO_RELEASE_IMPACT_PATHS' })
94
+ .describe('Comma-separated repo-root paths that count as an Expo change'),
95
+ ignoreExpoImpact: trueFalse('IGNORE_EXPO_IMPACT', 'false').describe(
96
+ 'Check the fingerprint even when no impact path changed'
97
+ ),
98
+ operation: z
99
+ .enum([
100
+ 'deploy',
101
+ 'force-native',
102
+ 'retry-ota',
103
+ 'republish',
104
+ 'rollback-embedded'
105
+ ])
106
+ .default('deploy')
107
+ .meta({ env: 'EXPO_RELEASE_OPERATION' })
108
+ .describe('What to run'),
109
+ deployType: z
110
+ .enum(['none', 'ota', 'native'])
111
+ .default('none')
112
+ .meta({ env: 'EXPO_RELEASE_DEPLOY_TYPE' })
113
+ .describe('Decision type for operate'),
114
+ channel: text('CHANNEL').describe(
115
+ 'EAS Update channel (default: the profile)'
116
+ ),
117
+ environment: text('ENVIRONMENT').describe(
118
+ 'EAS environment for eas update (required for Expo SDK 55+)'
119
+ ),
120
+ autoSubmit: trueFalse('AUTO_SUBMIT', 'true').describe(
121
+ 'Pass --auto-submit on native builds'
122
+ ),
123
+ preUpdate: text('PRE_UPDATE').describe(
124
+ 'Shell command run before eas update'
125
+ ),
126
+ postUpdate: text('POST_UPDATE').describe(
127
+ 'Shell command run after a successful eas update'
128
+ ),
129
+ updateExtraArgs: text('UPDATE_EXTRA_ARGS').describe(
130
+ 'Extra tokens appended to eas update'
131
+ ),
132
+ exportSourceMaps: trueFalse('EXPORT_SOURCE_MAPS', 'false').describe(
133
+ 'On OTA, expo export with external source maps, then publish dist'
134
+ ),
135
+ group: text('GROUP').describe('Update group ID for --operation republish'),
136
+ runtimeVersion: text('RUNTIME_VERSION').describe(
137
+ 'Runtime version for rollback-embedded (default: the fingerprint)'
138
+ ),
139
+ message: text('MESSAGE').describe('Override the eas update message'),
140
+ fingerprint: text('FINGERPRINT').describe(
141
+ 'Fingerprint hash to use instead of generating one'
142
+ ),
143
+ dryRun: z
144
+ .boolean()
145
+ .meta({ env: 'EXPO_RELEASE_DRY_RUN' })
146
+ .describe('Print mutating EAS commands without running them')
147
+ })
148
+ .superRefine((flags, ctx) => {
149
+ if (flags.operation === 'republish' && !flags.group)
150
+ ctx.addIssue({
151
+ code: 'custom',
152
+ path: ['group'],
153
+ message: 'required by --operation republish'
154
+ })
155
+ })
156
+
157
+ export type ReleaseFlags = z.output<typeof releaseFlags>
158
+
159
+ export function toReleaseOptions(
160
+ command: CommandName,
161
+ { context: flags, env }: CommandContext<ReleaseFlags>
162
+ ): ReleaseOptions {
163
+ return {
164
+ ...flags,
165
+ command,
166
+ base: flags.base ?? env.GITHUB_EVENT_BEFORE,
167
+ head: flags.head ?? env.GITHUB_SHA,
168
+ channel: flags.channel ?? flags.profile
110
169
  }
111
- throw new Error(USAGE)
112
170
  }
113
171
 
114
172
  export function needsDecide(operation: Operation): boolean {
@@ -137,192 +195,3 @@ export function shouldIgnoreExpoImpact(
137
195
  options.operation === 'deploy'
138
196
  )
139
197
  }
140
-
141
- export function parseReleaseOptions(
142
- args: string[],
143
- env: NodeJS.ProcessEnv = process.env
144
- ): ReleaseOptions {
145
- let values: {
146
- profile?: string
147
- base?: string
148
- head?: string
149
- platform?: string
150
- 'impact-paths'?: string
151
- 'ignore-expo-impact'?: string
152
- operation?: string
153
- 'deploy-type'?: string
154
- channel?: string
155
- environment?: string
156
- 'auto-submit'?: string
157
- 'pre-update'?: string
158
- 'post-update'?: string
159
- 'update-extra-args'?: string
160
- 'export-source-maps'?: string
161
- group?: string
162
- 'runtime-version'?: string
163
- message?: string
164
- fingerprint?: string
165
- 'dry-run'?: boolean
166
- }
167
- let positionals: string[]
168
-
169
- try {
170
- const parsed = parseArgs({
171
- args,
172
- allowPositionals: true,
173
- strict: true,
174
- options: {
175
- profile: { type: 'string' },
176
- base: { type: 'string' },
177
- head: { type: 'string' },
178
- platform: { type: 'string' },
179
- 'impact-paths': { type: 'string' },
180
- 'ignore-expo-impact': { type: 'string' },
181
- operation: { type: 'string' },
182
- 'deploy-type': { type: 'string' },
183
- channel: { type: 'string' },
184
- environment: { type: 'string' },
185
- 'auto-submit': { type: 'string' },
186
- 'pre-update': { type: 'string' },
187
- 'post-update': { type: 'string' },
188
- 'update-extra-args': { type: 'string' },
189
- 'export-source-maps': { type: 'string' },
190
- group: { type: 'string' },
191
- 'runtime-version': { type: 'string' },
192
- message: { type: 'string' },
193
- fingerprint: { type: 'string' },
194
- 'dry-run': { type: 'boolean', default: false }
195
- }
196
- })
197
- values = parsed.values
198
- positionals = parsed.positionals
199
- } catch (error) {
200
- const message = error instanceof Error ? error.message : String(error)
201
- throw new Error(`${USAGE}\n${message}`)
202
- }
203
-
204
- if (positionals.length > 1) {
205
- throw new Error(USAGE)
206
- }
207
-
208
- const command = parseCommandName(
209
- positionals[0] ?? fromEnv(env, 'EXPO_RELEASE_COMMAND')
210
- )
211
- const profile = firstDefined(
212
- values.profile,
213
- fromEnv(env, 'EXPO_RELEASE_PROFILE')
214
- )
215
- if (!profile) {
216
- throw new Error(USAGE)
217
- }
218
-
219
- const operation = parseOperation(
220
- firstDefined(values.operation, fromEnv(env, 'EXPO_RELEASE_OPERATION'))
221
- )
222
- const group = optionalString(
223
- firstDefined(values.group, fromEnv(env, 'EXPO_RELEASE_GROUP'))
224
- )
225
- if (operation === 'republish' && !group) {
226
- throw new Error(
227
- 'Usage: --operation republish requires --group <update-group-id>'
228
- )
229
- }
230
-
231
- const channel =
232
- firstDefined(values.channel, fromEnv(env, 'EXPO_RELEASE_CHANNEL')) ??
233
- profile
234
-
235
- return {
236
- command,
237
- profile,
238
- base: optionalString(
239
- firstDefined(
240
- values.base,
241
- fromEnv(env, 'EXPO_RELEASE_BASE'),
242
- fromEnv(env, 'GITHUB_EVENT_BEFORE')
243
- )
244
- ),
245
- head: optionalString(
246
- firstDefined(
247
- values.head,
248
- fromEnv(env, 'EXPO_RELEASE_HEAD'),
249
- fromEnv(env, 'GITHUB_SHA')
250
- )
251
- ),
252
- platform: parsePlatform(
253
- firstDefined(values.platform, fromEnv(env, 'EXPO_RELEASE_PLATFORM'))
254
- ),
255
- impactPaths: parseImpactPaths(
256
- firstDefined(
257
- values['impact-paths'],
258
- fromEnv(env, 'EXPO_RELEASE_IMPACT_PATHS')
259
- )
260
- ),
261
- ignoreExpoImpact: parseBoolean(
262
- firstDefined(
263
- values['ignore-expo-impact'],
264
- fromEnv(env, 'EXPO_RELEASE_IGNORE_EXPO_IMPACT')
265
- ),
266
- false
267
- ),
268
- operation,
269
- deployType: parseDeployType(
270
- firstDefined(
271
- values['deploy-type'],
272
- fromEnv(env, 'EXPO_RELEASE_DEPLOY_TYPE')
273
- )
274
- ),
275
- channel,
276
- environment: optionalString(
277
- firstDefined(values.environment, fromEnv(env, 'EXPO_RELEASE_ENVIRONMENT'))
278
- ),
279
- autoSubmit: parseBoolean(
280
- firstDefined(
281
- values['auto-submit'],
282
- fromEnv(env, 'EXPO_RELEASE_AUTO_SUBMIT')
283
- ),
284
- true
285
- ),
286
- preUpdate: optionalString(
287
- firstDefined(
288
- values['pre-update'],
289
- fromEnv(env, 'EXPO_RELEASE_PRE_UPDATE')
290
- )
291
- ),
292
- postUpdate: optionalString(
293
- firstDefined(
294
- values['post-update'],
295
- fromEnv(env, 'EXPO_RELEASE_POST_UPDATE')
296
- )
297
- ),
298
- updateExtraArgs: optionalString(
299
- firstDefined(
300
- values['update-extra-args'],
301
- fromEnv(env, 'EXPO_RELEASE_UPDATE_EXTRA_ARGS')
302
- )
303
- ),
304
- exportSourceMaps: parseBoolean(
305
- firstDefined(
306
- values['export-source-maps'],
307
- fromEnv(env, 'EXPO_RELEASE_EXPORT_SOURCE_MAPS')
308
- ),
309
- false
310
- ),
311
- group,
312
- runtimeVersion: optionalString(
313
- firstDefined(
314
- values['runtime-version'],
315
- fromEnv(env, 'EXPO_RELEASE_RUNTIME_VERSION')
316
- )
317
- ),
318
- message: optionalString(
319
- firstDefined(values.message, fromEnv(env, 'EXPO_RELEASE_MESSAGE'))
320
- ),
321
- fingerprint: optionalString(
322
- firstDefined(values.fingerprint, fromEnv(env, 'EXPO_RELEASE_FINGERPRINT'))
323
- ),
324
- dryRun:
325
- values['dry-run'] === true ||
326
- parseBoolean(fromEnv(env, 'EXPO_RELEASE_DRY_RUN'), false)
327
- }
328
- }