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.
- package/README.md +25 -5
- package/action.yml +6 -0
- package/dist/args.d.ts +48 -9
- package/dist/cli.js +101 -170
- package/dist/index.js +54 -130
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +65 -20
- package/dist/runtime/push.d.ts +19 -7
- package/package.json +5 -1
- package/src/args.ts +128 -259
- package/src/cli.ts +77 -47
- package/src/runtime/index.ts +1 -0
- package/src/runtime/push.ts +98 -21
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ Reference the upstream README for full input/output docs and recovery operations
|
|
|
70
70
|
uses: HansKristoffer/expo-release@v2
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
The action runs `bun ${{ github.action_path }}/src/cli.ts`. The caller must already have checked out the repo (`fetch-depth: 0`), installed Bun, set up EAS, and installed workspace dependencies.
|
|
73
|
+
The action installs its own dependencies (`hanzio`, `zod`) in `${{ github.action_path }}`, then runs `bun ${{ github.action_path }}/src/cli.ts`. The caller must already have checked out the repo (`fetch-depth: 0`), installed Bun, set up EAS, and installed workspace dependencies.
|
|
74
74
|
|
|
75
75
|
### npm (CLI)
|
|
76
76
|
|
|
@@ -425,6 +425,8 @@ expo-release operate \
|
|
|
425
425
|
[--post-update "bunx posthog-cli hermes upload --directory dist"]
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
+
Run `expo-release <command> --help` for every flag. Each flag also reads `EXPO_RELEASE_<FLAG>` (for example `EXPO_RELEASE_IMPACT_PATHS`) when it is not passed; an empty value counts as unset.
|
|
429
|
+
|
|
428
430
|
`release` is decide + operate (same as the action default). `force-native`, `republish`, and `rollback-embedded` skip decide. `decide` prints JSON including `deployType` and `nativeBuildPending`. When `GITHUB_OUTPUT` is set, decide writes `deploy_type`, `native_build_pending`, `changed`, `fingerprint`, `build_id`, `build_url`. Operate writes `result`.
|
|
429
431
|
|
|
430
432
|
```bash
|
|
@@ -490,12 +492,30 @@ export const push = createPushNotifications({
|
|
|
490
492
|
| Member | Use |
|
|
491
493
|
|--------|-----|
|
|
492
494
|
| `prompt()` | After sign-in. Asks once per install (soft-ask, then OS dialog), registers the token on a grant. Never throws; concurrent calls share one dialog. |
|
|
493
|
-
| `enable()` / `disable()` | Settings toggle. `enable` requests permission and registers; `disable` calls `registerDevice(null)`.
|
|
494
|
-
| `syncToken()` |
|
|
495
|
-
| `listen({ onReceived, onResponse })` | Subscribes and returns the unsubscribe function. `onResponse` also receives the tap that launched the app. |
|
|
495
|
+
| `enable()` / `disable()` | Settings toggle. `enable` requests permission and registers; `disable` calls `registerDevice(null)`. Use `disable()` to turn push off (also on sign-out) so the token is not re-sent on the next launch. |
|
|
496
|
+
| `syncToken()` | Registers the current token when permission is granted. |
|
|
497
|
+
| `listen({ onReceived, onResponse })` | Subscribes and returns the unsubscribe function. `onResponse` also receives the tap that launched the app. Also keeps the token current: on start and whenever Expo rotates it, a token that differs from the last one sent is registered again. This runs only after a token was sent and `disable()` has not run since. Call it once at the root of the app. |
|
|
496
498
|
| `getPermissionStatus()`, `getPushToken()`, `requestPermissions()`, `hasPrompted()` | Lower-level steps. |
|
|
497
499
|
|
|
498
|
-
`usePushNotifications(push, { enabled })` gives a settings screen `permission`, `isEnabled`, `isLoading` and wrapped `prompt`/`enable`/`disable`. `enabled` is the saved preference; while it is on, the token is re-registered on mount.
|
|
500
|
+
`usePushNotifications(push, { enabled, persist })` gives a settings screen `permission`, `isEnabled`, `isLoading` and wrapped `prompt`/`enable`/`disable`. `enabled` is the saved preference; while it is on, the token is re-registered on mount.
|
|
501
|
+
|
|
502
|
+
`persist` saves the preference, for example on the user's account. `enable()` and `disable()` call it first and show the new value right away. If it rejects, the hook restores `enabled`, skips the push step and rethrows, so catch it for your error toast. Saving before `enable()` means a denied dialog leaves the preference on with `isEnabled` false; the token registers on the next mount once permission is granted in device settings. Without `persist`, save the preference yourself before calling `enable()`.
|
|
503
|
+
|
|
504
|
+
```tsx
|
|
505
|
+
const { isEnabled, isLoading, enable, disable } = usePushNotifications(push, {
|
|
506
|
+
enabled: user.pushEnabled,
|
|
507
|
+
persist: (pushEnabled) => api.user.update({ pushEnabled })
|
|
508
|
+
})
|
|
509
|
+
|
|
510
|
+
const onToggle = async (on: boolean) => {
|
|
511
|
+
try {
|
|
512
|
+
if (!on) return await disable()
|
|
513
|
+
if (!(await enable())) toast('Allow notifications in device settings')
|
|
514
|
+
} catch {
|
|
515
|
+
toast('Could not save your notification setting')
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
499
519
|
|
|
500
520
|
### Error boundary
|
|
501
521
|
|
package/action.yml
CHANGED
|
@@ -107,6 +107,12 @@ outputs:
|
|
|
107
107
|
runs:
|
|
108
108
|
using: composite
|
|
109
109
|
steps:
|
|
110
|
+
# The CLI imports hanzio and zod; the action checkout has no node_modules.
|
|
111
|
+
- name: Install expo-release dependencies
|
|
112
|
+
working-directory: ${{ github.action_path }}
|
|
113
|
+
shell: bash
|
|
114
|
+
run: bun install --production --frozen-lockfile --omit peer
|
|
115
|
+
|
|
110
116
|
- name: Expo release
|
|
111
117
|
id: release
|
|
112
118
|
working-directory: ${{ inputs.working-directory }}
|
package/dist/args.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CommandContext } from 'hanzio/cli';
|
|
2
|
+
import { z } from 'zod';
|
|
1
3
|
export declare const DEFAULT_IMPACT_PATHS: readonly ['apps/expo/'];
|
|
2
4
|
export type ReleasePlatform = 'ios' | 'android';
|
|
3
5
|
export type CommandName = 'release' | 'decide' | 'operate';
|
|
@@ -26,14 +28,51 @@ export type ReleaseOptions = {
|
|
|
26
28
|
fingerprint?: string;
|
|
27
29
|
dryRun: boolean;
|
|
28
30
|
};
|
|
29
|
-
export declare const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
export declare const releaseFlags: z.ZodObject<{
|
|
32
|
+
profile: z.ZodString;
|
|
33
|
+
base: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
34
|
+
head: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
35
|
+
platform: z.ZodDefault<z.ZodEnum<{
|
|
36
|
+
android: "android";
|
|
37
|
+
ios: "ios";
|
|
38
|
+
}>>;
|
|
39
|
+
impactPaths: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string[], string>>;
|
|
40
|
+
ignoreExpoImpact: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
41
|
+
false: "false";
|
|
42
|
+
true: "true";
|
|
43
|
+
}>>, z.ZodTransform<boolean, "false" | "true">>;
|
|
44
|
+
operation: z.ZodDefault<z.ZodEnum<{
|
|
45
|
+
deploy: "deploy";
|
|
46
|
+
"force-native": "force-native";
|
|
47
|
+
republish: "republish";
|
|
48
|
+
"retry-ota": "retry-ota";
|
|
49
|
+
"rollback-embedded": "rollback-embedded";
|
|
50
|
+
}>>;
|
|
51
|
+
deployType: z.ZodDefault<z.ZodEnum<{
|
|
52
|
+
native: "native";
|
|
53
|
+
none: "none";
|
|
54
|
+
ota: "ota";
|
|
55
|
+
}>>;
|
|
56
|
+
channel: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
57
|
+
environment: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
58
|
+
autoSubmit: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
59
|
+
false: "false";
|
|
60
|
+
true: "true";
|
|
61
|
+
}>>, z.ZodTransform<boolean, "false" | "true">>;
|
|
62
|
+
preUpdate: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
63
|
+
postUpdate: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
64
|
+
updateExtraArgs: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
65
|
+
exportSourceMaps: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
|
|
66
|
+
false: "false";
|
|
67
|
+
true: "true";
|
|
68
|
+
}>>, z.ZodTransform<boolean, "false" | "true">>;
|
|
69
|
+
group: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
70
|
+
runtimeVersion: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
71
|
+
message: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
72
|
+
fingerprint: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
|
|
73
|
+
dryRun: z.ZodBoolean;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
export type ReleaseFlags = z.output<typeof releaseFlags>;
|
|
76
|
+
export declare function toReleaseOptions(command: CommandName, { context: flags, env }: CommandContext<ReleaseFlags>): ReleaseOptions;
|
|
37
77
|
export declare function needsDecide(operation: Operation): boolean;
|
|
38
78
|
export declare function shouldIgnoreExpoImpact(options: Pick<ReleaseOptions, 'ignoreExpoImpact' | 'operation'>): boolean;
|
|
39
|
-
export declare function parseReleaseOptions(args: string[], env?: NodeJS.ProcessEnv): ReleaseOptions;
|
package/dist/cli.js
CHANGED
|
@@ -1,62 +1,65 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
import { defineCli, defineCommand } from "hanzio/cli";
|
|
6
|
+
|
|
4
7
|
// src/args.ts
|
|
5
|
-
import {
|
|
8
|
+
import { z } from "zod";
|
|
6
9
|
var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
10
|
+
var text = (env) => z.string().transform((value) => value.trim() || undefined).optional().meta({ env: `EXPO_RELEASE_${env}` });
|
|
11
|
+
var trueFalse = (env, fallback) => z.enum(["true", "false"]).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
|
|
12
|
+
var releaseFlags = z.object({
|
|
13
|
+
profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
|
|
14
|
+
base: text("BASE").describe("Git base SHA for the impact diff (falls back to GITHUB_EVENT_BEFORE)"),
|
|
15
|
+
head: text("HEAD").describe("Git head SHA for the impact diff (falls back to GITHUB_SHA)"),
|
|
16
|
+
platform: z.enum(["ios", "android"]).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
17
|
+
impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
|
|
18
|
+
const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
19
|
+
if (paths.length === 0)
|
|
20
|
+
ctx.addIssue({
|
|
21
|
+
code: "custom",
|
|
22
|
+
message: "At least one --impact-paths entry is required"
|
|
23
|
+
});
|
|
24
|
+
return paths;
|
|
25
|
+
}).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
|
|
26
|
+
ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
|
|
27
|
+
operation: z.enum([
|
|
28
|
+
"deploy",
|
|
29
|
+
"force-native",
|
|
30
|
+
"retry-ota",
|
|
31
|
+
"republish",
|
|
32
|
+
"rollback-embedded"
|
|
33
|
+
]).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
34
|
+
deployType: z.enum(["none", "ota", "native"]).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
35
|
+
channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
|
|
36
|
+
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+)"),
|
|
37
|
+
autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
|
|
38
|
+
preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
|
|
39
|
+
postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
|
|
40
|
+
updateExtraArgs: text("UPDATE_EXTRA_ARGS").describe("Extra tokens appended to eas update"),
|
|
41
|
+
exportSourceMaps: trueFalse("EXPORT_SOURCE_MAPS", "false").describe("On OTA, expo export with external source maps, then publish dist"),
|
|
42
|
+
group: text("GROUP").describe("Update group ID for --operation republish"),
|
|
43
|
+
runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
|
|
44
|
+
message: text("MESSAGE").describe("Override the eas update message"),
|
|
45
|
+
fingerprint: text("FINGERPRINT").describe("Fingerprint hash to use instead of generating one"),
|
|
46
|
+
dryRun: z.boolean().meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
|
|
47
|
+
}).superRefine((flags, ctx) => {
|
|
48
|
+
if (flags.operation === "republish" && !flags.group)
|
|
49
|
+
ctx.addIssue({
|
|
50
|
+
code: "custom",
|
|
51
|
+
path: ["group"],
|
|
52
|
+
message: "required by --operation republish"
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
function toReleaseOptions(command, { context: flags, env }) {
|
|
56
|
+
return {
|
|
57
|
+
...flags,
|
|
58
|
+
command,
|
|
59
|
+
base: flags.base ?? env.GITHUB_EVENT_BEFORE,
|
|
60
|
+
head: flags.head ?? env.GITHUB_SHA,
|
|
61
|
+
channel: flags.channel ?? flags.profile
|
|
62
|
+
};
|
|
60
63
|
}
|
|
61
64
|
function needsDecide(operation) {
|
|
62
65
|
switch (operation) {
|
|
@@ -80,82 +83,6 @@ function shouldIgnoreExpoImpact(options) {
|
|
|
80
83
|
return true;
|
|
81
84
|
return process.env.GITHUB_EVENT_NAME === "workflow_dispatch" && options.operation === "deploy";
|
|
82
85
|
}
|
|
83
|
-
function parseReleaseOptions(args, env = process.env) {
|
|
84
|
-
let values;
|
|
85
|
-
let positionals;
|
|
86
|
-
try {
|
|
87
|
-
const parsed = parseArgs({
|
|
88
|
-
args,
|
|
89
|
-
allowPositionals: true,
|
|
90
|
-
strict: true,
|
|
91
|
-
options: {
|
|
92
|
-
profile: { type: "string" },
|
|
93
|
-
base: { type: "string" },
|
|
94
|
-
head: { type: "string" },
|
|
95
|
-
platform: { type: "string" },
|
|
96
|
-
"impact-paths": { type: "string" },
|
|
97
|
-
"ignore-expo-impact": { type: "string" },
|
|
98
|
-
operation: { type: "string" },
|
|
99
|
-
"deploy-type": { type: "string" },
|
|
100
|
-
channel: { type: "string" },
|
|
101
|
-
environment: { type: "string" },
|
|
102
|
-
"auto-submit": { type: "string" },
|
|
103
|
-
"pre-update": { type: "string" },
|
|
104
|
-
"post-update": { type: "string" },
|
|
105
|
-
"update-extra-args": { type: "string" },
|
|
106
|
-
"export-source-maps": { type: "string" },
|
|
107
|
-
group: { type: "string" },
|
|
108
|
-
"runtime-version": { type: "string" },
|
|
109
|
-
message: { type: "string" },
|
|
110
|
-
fingerprint: { type: "string" },
|
|
111
|
-
"dry-run": { type: "boolean", default: false }
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
values = parsed.values;
|
|
115
|
-
positionals = parsed.positionals;
|
|
116
|
-
} catch (error) {
|
|
117
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
-
throw new Error(`${USAGE}
|
|
119
|
-
${message}`);
|
|
120
|
-
}
|
|
121
|
-
if (positionals.length > 1) {
|
|
122
|
-
throw new Error(USAGE);
|
|
123
|
-
}
|
|
124
|
-
const command = parseCommandName(positionals[0] ?? fromEnv(env, "EXPO_RELEASE_COMMAND"));
|
|
125
|
-
const profile = firstDefined(values.profile, fromEnv(env, "EXPO_RELEASE_PROFILE"));
|
|
126
|
-
if (!profile) {
|
|
127
|
-
throw new Error(USAGE);
|
|
128
|
-
}
|
|
129
|
-
const operation = parseOperation(firstDefined(values.operation, fromEnv(env, "EXPO_RELEASE_OPERATION")));
|
|
130
|
-
const group = optionalString(firstDefined(values.group, fromEnv(env, "EXPO_RELEASE_GROUP")));
|
|
131
|
-
if (operation === "republish" && !group) {
|
|
132
|
-
throw new Error("Usage: --operation republish requires --group <update-group-id>");
|
|
133
|
-
}
|
|
134
|
-
const channel = firstDefined(values.channel, fromEnv(env, "EXPO_RELEASE_CHANNEL")) ?? profile;
|
|
135
|
-
return {
|
|
136
|
-
command,
|
|
137
|
-
profile,
|
|
138
|
-
base: optionalString(firstDefined(values.base, fromEnv(env, "EXPO_RELEASE_BASE"), fromEnv(env, "GITHUB_EVENT_BEFORE"))),
|
|
139
|
-
head: optionalString(firstDefined(values.head, fromEnv(env, "EXPO_RELEASE_HEAD"), fromEnv(env, "GITHUB_SHA"))),
|
|
140
|
-
platform: parsePlatform(firstDefined(values.platform, fromEnv(env, "EXPO_RELEASE_PLATFORM"))),
|
|
141
|
-
impactPaths: parseImpactPaths(firstDefined(values["impact-paths"], fromEnv(env, "EXPO_RELEASE_IMPACT_PATHS"))),
|
|
142
|
-
ignoreExpoImpact: parseBoolean(firstDefined(values["ignore-expo-impact"], fromEnv(env, "EXPO_RELEASE_IGNORE_EXPO_IMPACT")), false),
|
|
143
|
-
operation,
|
|
144
|
-
deployType: parseDeployType(firstDefined(values["deploy-type"], fromEnv(env, "EXPO_RELEASE_DEPLOY_TYPE"))),
|
|
145
|
-
channel,
|
|
146
|
-
environment: optionalString(firstDefined(values.environment, fromEnv(env, "EXPO_RELEASE_ENVIRONMENT"))),
|
|
147
|
-
autoSubmit: parseBoolean(firstDefined(values["auto-submit"], fromEnv(env, "EXPO_RELEASE_AUTO_SUBMIT")), true),
|
|
148
|
-
preUpdate: optionalString(firstDefined(values["pre-update"], fromEnv(env, "EXPO_RELEASE_PRE_UPDATE"))),
|
|
149
|
-
postUpdate: optionalString(firstDefined(values["post-update"], fromEnv(env, "EXPO_RELEASE_POST_UPDATE"))),
|
|
150
|
-
updateExtraArgs: optionalString(firstDefined(values["update-extra-args"], fromEnv(env, "EXPO_RELEASE_UPDATE_EXTRA_ARGS"))),
|
|
151
|
-
exportSourceMaps: parseBoolean(firstDefined(values["export-source-maps"], fromEnv(env, "EXPO_RELEASE_EXPORT_SOURCE_MAPS")), false),
|
|
152
|
-
group,
|
|
153
|
-
runtimeVersion: optionalString(firstDefined(values["runtime-version"], fromEnv(env, "EXPO_RELEASE_RUNTIME_VERSION"))),
|
|
154
|
-
message: optionalString(firstDefined(values.message, fromEnv(env, "EXPO_RELEASE_MESSAGE"))),
|
|
155
|
-
fingerprint: optionalString(firstDefined(values.fingerprint, fromEnv(env, "EXPO_RELEASE_FINGERPRINT"))),
|
|
156
|
-
dryRun: values["dry-run"] === true || parseBoolean(fromEnv(env, "EXPO_RELEASE_DRY_RUN"), false)
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
86
|
|
|
160
87
|
// src/github-output.ts
|
|
161
88
|
import { appendFile } from "fs/promises";
|
|
@@ -628,48 +555,52 @@ function toOperationInput(options, decision) {
|
|
|
628
555
|
dryRun: options.dryRun
|
|
629
556
|
};
|
|
630
557
|
}
|
|
631
|
-
async function
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
process.stdout.write(`${USAGE}
|
|
636
|
-
`);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
const options = parseReleaseOptions(raw);
|
|
640
|
-
switch (options.command) {
|
|
641
|
-
case "decide": {
|
|
642
|
-
const decision = await decideRelease(toDecisionInput(options));
|
|
643
|
-
await writeDecideGitHubOutput(decision);
|
|
644
|
-
process.stdout.write(`${JSON.stringify(decision)}
|
|
558
|
+
async function decide(options, ctx) {
|
|
559
|
+
const decision = await decideRelease(toDecisionInput(options));
|
|
560
|
+
await writeDecideGitHubOutput(decision);
|
|
561
|
+
ctx.stdout.write(`${JSON.stringify(decision)}
|
|
645
562
|
`);
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
`);
|
|
653
|
-
return;
|
|
654
|
-
}
|
|
655
|
-
case "release": {
|
|
656
|
-
let decision = null;
|
|
657
|
-
if (needsDecide(options.operation)) {
|
|
658
|
-
decision = await decideRelease(toDecisionInput(options));
|
|
659
|
-
await writeDecideGitHubOutput(decision);
|
|
660
|
-
process.stdout.write(`${JSON.stringify(decision)}
|
|
563
|
+
return decision;
|
|
564
|
+
}
|
|
565
|
+
async function operate(options, decision, ctx) {
|
|
566
|
+
const result = await operateRelease(toOperationInput(options, decision));
|
|
567
|
+
await writeOperateGitHubOutput(result);
|
|
568
|
+
ctx.stdout.write(`${result}
|
|
661
569
|
`);
|
|
570
|
+
}
|
|
571
|
+
var COMMANDS = ["release", "decide", "operate"];
|
|
572
|
+
var cli = defineCli({
|
|
573
|
+
name: "expo-release",
|
|
574
|
+
description: "Fingerprint-driven Expo EAS release: OTA update or native store build",
|
|
575
|
+
globals: releaseFlags,
|
|
576
|
+
context: (flags) => flags,
|
|
577
|
+
commands: {
|
|
578
|
+
release: defineCommand({
|
|
579
|
+
description: "Run decide, then operate on its decision",
|
|
580
|
+
run: async (_input, ctx) => {
|
|
581
|
+
const options = toReleaseOptions("release", ctx);
|
|
582
|
+
const decision = needsDecide(options.operation) ? await decide(options, ctx) : null;
|
|
583
|
+
await operate(options, decision, ctx);
|
|
662
584
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
585
|
+
}),
|
|
586
|
+
decide: defineCommand({
|
|
587
|
+
description: "Print whether this change needs no deploy, an OTA update or a native build",
|
|
588
|
+
run: async (_input, ctx) => {
|
|
589
|
+
await decide(toReleaseOptions("decide", ctx), ctx);
|
|
590
|
+
}
|
|
591
|
+
}),
|
|
592
|
+
operate: defineCommand({
|
|
593
|
+
description: "Run --operation for --deploy-type",
|
|
594
|
+
run: async (_input, ctx) => {
|
|
595
|
+
await operate(toReleaseOptions("operate", ctx), null, ctx);
|
|
596
|
+
}
|
|
597
|
+
})
|
|
673
598
|
}
|
|
599
|
+
});
|
|
600
|
+
var env = Object.fromEntries(Object.entries(process.env).flatMap(([name, value]) => value?.trim() ? [[name, value.trim()]] : []));
|
|
601
|
+
var argv = process.argv.slice(2);
|
|
602
|
+
if (!argv.some((arg) => COMMANDS.includes(arg))) {
|
|
603
|
+
argv = env.EXPO_RELEASE_COMMAND ? [env.EXPO_RELEASE_COMMAND, ...argv] : argv.length === 0 ? ["--help"] : argv;
|
|
674
604
|
}
|
|
675
|
-
await
|
|
605
|
+
var code = await cli.run(argv, { env });
|
|
606
|
+
process.exitCode = code === 2 ? 1 : code;
|
package/dist/index.js
CHANGED
|
@@ -1,60 +1,60 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/args.ts
|
|
3
|
-
import {
|
|
3
|
+
import { z } from "zod";
|
|
4
4
|
var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
5
|
+
var text = (env) => z.string().transform((value) => value.trim() || undefined).optional().meta({ env: `EXPO_RELEASE_${env}` });
|
|
6
|
+
var trueFalse = (env, fallback) => z.enum(["true", "false"]).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
|
|
7
|
+
var releaseFlags = z.object({
|
|
8
|
+
profile: z.string().trim().min(1).meta({ env: "EXPO_RELEASE_PROFILE" }).describe("EAS build profile, also the default update channel"),
|
|
9
|
+
base: text("BASE").describe("Git base SHA for the impact diff (falls back to GITHUB_EVENT_BEFORE)"),
|
|
10
|
+
head: text("HEAD").describe("Git head SHA for the impact diff (falls back to GITHUB_SHA)"),
|
|
11
|
+
platform: z.enum(["ios", "android"]).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
|
|
12
|
+
impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
|
|
13
|
+
const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
14
|
+
if (paths.length === 0)
|
|
15
|
+
ctx.addIssue({
|
|
16
|
+
code: "custom",
|
|
17
|
+
message: "At least one --impact-paths entry is required"
|
|
18
|
+
});
|
|
19
|
+
return paths;
|
|
20
|
+
}).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
|
|
21
|
+
ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
|
|
22
|
+
operation: z.enum([
|
|
23
|
+
"deploy",
|
|
24
|
+
"force-native",
|
|
25
|
+
"retry-ota",
|
|
26
|
+
"republish",
|
|
27
|
+
"rollback-embedded"
|
|
28
|
+
]).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
|
|
29
|
+
deployType: z.enum(["none", "ota", "native"]).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
|
|
30
|
+
channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
|
|
31
|
+
environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+)"),
|
|
32
|
+
autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
|
|
33
|
+
preUpdate: text("PRE_UPDATE").describe("Shell command run before eas update"),
|
|
34
|
+
postUpdate: text("POST_UPDATE").describe("Shell command run after a successful eas update"),
|
|
35
|
+
updateExtraArgs: text("UPDATE_EXTRA_ARGS").describe("Extra tokens appended to eas update"),
|
|
36
|
+
exportSourceMaps: trueFalse("EXPORT_SOURCE_MAPS", "false").describe("On OTA, expo export with external source maps, then publish dist"),
|
|
37
|
+
group: text("GROUP").describe("Update group ID for --operation republish"),
|
|
38
|
+
runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
|
|
39
|
+
message: text("MESSAGE").describe("Override the eas update message"),
|
|
40
|
+
fingerprint: text("FINGERPRINT").describe("Fingerprint hash to use instead of generating one"),
|
|
41
|
+
dryRun: z.boolean().meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
|
|
42
|
+
}).superRefine((flags, ctx) => {
|
|
43
|
+
if (flags.operation === "republish" && !flags.group)
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: "custom",
|
|
46
|
+
path: ["group"],
|
|
47
|
+
message: "required by --operation republish"
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
function toReleaseOptions(command, { context: flags, env }) {
|
|
51
|
+
return {
|
|
52
|
+
...flags,
|
|
53
|
+
command,
|
|
54
|
+
base: flags.base ?? env.GITHUB_EVENT_BEFORE,
|
|
55
|
+
head: flags.head ?? env.GITHUB_SHA,
|
|
56
|
+
channel: flags.channel ?? flags.profile
|
|
57
|
+
};
|
|
58
58
|
}
|
|
59
59
|
function needsDecide(operation) {
|
|
60
60
|
switch (operation) {
|
|
@@ -78,82 +78,6 @@ function shouldIgnoreExpoImpact(options) {
|
|
|
78
78
|
return true;
|
|
79
79
|
return process.env.GITHUB_EVENT_NAME === "workflow_dispatch" && options.operation === "deploy";
|
|
80
80
|
}
|
|
81
|
-
function parseReleaseOptions(args, env = process.env) {
|
|
82
|
-
let values;
|
|
83
|
-
let positionals;
|
|
84
|
-
try {
|
|
85
|
-
const parsed = parseArgs({
|
|
86
|
-
args,
|
|
87
|
-
allowPositionals: true,
|
|
88
|
-
strict: true,
|
|
89
|
-
options: {
|
|
90
|
-
profile: { type: "string" },
|
|
91
|
-
base: { type: "string" },
|
|
92
|
-
head: { type: "string" },
|
|
93
|
-
platform: { type: "string" },
|
|
94
|
-
"impact-paths": { type: "string" },
|
|
95
|
-
"ignore-expo-impact": { type: "string" },
|
|
96
|
-
operation: { type: "string" },
|
|
97
|
-
"deploy-type": { type: "string" },
|
|
98
|
-
channel: { type: "string" },
|
|
99
|
-
environment: { type: "string" },
|
|
100
|
-
"auto-submit": { type: "string" },
|
|
101
|
-
"pre-update": { type: "string" },
|
|
102
|
-
"post-update": { type: "string" },
|
|
103
|
-
"update-extra-args": { type: "string" },
|
|
104
|
-
"export-source-maps": { type: "string" },
|
|
105
|
-
group: { type: "string" },
|
|
106
|
-
"runtime-version": { type: "string" },
|
|
107
|
-
message: { type: "string" },
|
|
108
|
-
fingerprint: { type: "string" },
|
|
109
|
-
"dry-run": { type: "boolean", default: false }
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
values = parsed.values;
|
|
113
|
-
positionals = parsed.positionals;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
-
throw new Error(`${USAGE}
|
|
117
|
-
${message}`);
|
|
118
|
-
}
|
|
119
|
-
if (positionals.length > 1) {
|
|
120
|
-
throw new Error(USAGE);
|
|
121
|
-
}
|
|
122
|
-
const command = parseCommandName(positionals[0] ?? fromEnv(env, "EXPO_RELEASE_COMMAND"));
|
|
123
|
-
const profile = firstDefined(values.profile, fromEnv(env, "EXPO_RELEASE_PROFILE"));
|
|
124
|
-
if (!profile) {
|
|
125
|
-
throw new Error(USAGE);
|
|
126
|
-
}
|
|
127
|
-
const operation = parseOperation(firstDefined(values.operation, fromEnv(env, "EXPO_RELEASE_OPERATION")));
|
|
128
|
-
const group = optionalString(firstDefined(values.group, fromEnv(env, "EXPO_RELEASE_GROUP")));
|
|
129
|
-
if (operation === "republish" && !group) {
|
|
130
|
-
throw new Error("Usage: --operation republish requires --group <update-group-id>");
|
|
131
|
-
}
|
|
132
|
-
const channel = firstDefined(values.channel, fromEnv(env, "EXPO_RELEASE_CHANNEL")) ?? profile;
|
|
133
|
-
return {
|
|
134
|
-
command,
|
|
135
|
-
profile,
|
|
136
|
-
base: optionalString(firstDefined(values.base, fromEnv(env, "EXPO_RELEASE_BASE"), fromEnv(env, "GITHUB_EVENT_BEFORE"))),
|
|
137
|
-
head: optionalString(firstDefined(values.head, fromEnv(env, "EXPO_RELEASE_HEAD"), fromEnv(env, "GITHUB_SHA"))),
|
|
138
|
-
platform: parsePlatform(firstDefined(values.platform, fromEnv(env, "EXPO_RELEASE_PLATFORM"))),
|
|
139
|
-
impactPaths: parseImpactPaths(firstDefined(values["impact-paths"], fromEnv(env, "EXPO_RELEASE_IMPACT_PATHS"))),
|
|
140
|
-
ignoreExpoImpact: parseBoolean(firstDefined(values["ignore-expo-impact"], fromEnv(env, "EXPO_RELEASE_IGNORE_EXPO_IMPACT")), false),
|
|
141
|
-
operation,
|
|
142
|
-
deployType: parseDeployType(firstDefined(values["deploy-type"], fromEnv(env, "EXPO_RELEASE_DEPLOY_TYPE"))),
|
|
143
|
-
channel,
|
|
144
|
-
environment: optionalString(firstDefined(values.environment, fromEnv(env, "EXPO_RELEASE_ENVIRONMENT"))),
|
|
145
|
-
autoSubmit: parseBoolean(firstDefined(values["auto-submit"], fromEnv(env, "EXPO_RELEASE_AUTO_SUBMIT")), true),
|
|
146
|
-
preUpdate: optionalString(firstDefined(values["pre-update"], fromEnv(env, "EXPO_RELEASE_PRE_UPDATE"))),
|
|
147
|
-
postUpdate: optionalString(firstDefined(values["post-update"], fromEnv(env, "EXPO_RELEASE_POST_UPDATE"))),
|
|
148
|
-
updateExtraArgs: optionalString(firstDefined(values["update-extra-args"], fromEnv(env, "EXPO_RELEASE_UPDATE_EXTRA_ARGS"))),
|
|
149
|
-
exportSourceMaps: parseBoolean(firstDefined(values["export-source-maps"], fromEnv(env, "EXPO_RELEASE_EXPORT_SOURCE_MAPS")), false),
|
|
150
|
-
group,
|
|
151
|
-
runtimeVersion: optionalString(firstDefined(values["runtime-version"], fromEnv(env, "EXPO_RELEASE_RUNTIME_VERSION"))),
|
|
152
|
-
message: optionalString(firstDefined(values.message, fromEnv(env, "EXPO_RELEASE_MESSAGE"))),
|
|
153
|
-
fingerprint: optionalString(firstDefined(values.fingerprint, fromEnv(env, "EXPO_RELEASE_FINGERPRINT"))),
|
|
154
|
-
dryRun: values["dry-run"] === true || parseBoolean(fromEnv(env, "EXPO_RELEASE_DRY_RUN"), false)
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
81
|
// src/github-output.ts
|
|
158
82
|
import { appendFile } from "fs/promises";
|
|
159
83
|
async function appendGitHubOutput(entries) {
|