expo-release 2.3.0 → 2.3.2

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 CHANGED
@@ -540,9 +540,9 @@ Composite actions for Bun monorepos, used as `HansKristoffer/expo-release/.githu
540
540
 
541
541
  | Action | Does |
542
542
  |--------|------|
543
- | `setup-workspace` | Bun (version from `package.json` `packageManager` by default), optional Node, restore the dependency cache keyed on `bun.lock`, always `bun install --frozen-lockfile`, save on a miss with `save-cache: 'true'`. `cache-paths` and `cache-key-files` are inputs. |
544
- | `playwright-browsers` | Restores browsers for the installed Playwright version; installs and saves on a miss. |
545
- | `pr-preview` | Railway preview environment per PR with its own PlanetScale Postgres branch and a Cloudflare-managed host. Creates on open/sync, deletes on close. |
543
+ | `setup-workspace` | Bun (version from `package.json` `packageManager` by default), optional Node, restore the dependency cache keyed on `bun.lock`, always `bun install --frozen-lockfile`, save on a miss with `save-cache: 'true'`. `cache-paths` and `cache-key-files` are inputs. On a self-hosted runner it keeps `cache-paths` instead (see below). |
544
+ | `playwright-browsers` | Restores browsers for the installed Playwright version; installs and saves on a miss. A self-hosted runner skips the cache and keeps `~/.cache/ms-playwright`. |
545
+ | `pr-preview` | Railway preview environment per PR with its own PlanetScale Postgres branch and a Cloudflare-managed host. Creates on open/sync, deletes on close. The Railway CLI goes into the job's `$RUNNER_TEMP`. |
546
546
  | `ai-changelog` | Plain-language changelog of a pushed range with Claude; outputs `message` (chat) and `markdown`, optionally POSTs the message to a webhook. |
547
547
 
548
548
  ### Warm the caches on main
@@ -580,6 +580,29 @@ jobs:
580
580
 
581
581
  Use the same `cache-paths` in every job, since they are part of the cache version.
582
582
 
583
+ ### Self-hosted runners
584
+
585
+ A self-hosted runner keeps its workspace between jobs, so these actions keep what a hosted VM would restore. Check out with `clean: false`; `setup-workspace` then removes every untracked file except `cache-paths` and `keep-paths`, skips the cache, and skips `bun install` when the files named by `install-key-paths` and the Bun version are unchanged since that runner's last install. Add the files a postinstall reads:
586
+
587
+ ```yaml
588
+ - uses: actions/checkout@v5
589
+ with:
590
+ clean: false
591
+ - uses: HansKristoffer/expo-release/.github/actions/setup-workspace@v2
592
+ with:
593
+ cache-paths: |
594
+ node_modules
595
+ packages/*/node_modules
596
+ apps/*/node_modules
597
+ packages/prisma/generated
598
+ keep-paths: .buncargo/typecheck-timings.json
599
+ install-key-paths: |
600
+ bun.lock
601
+ *package.json
602
+ patches
603
+ packages/prisma/schema
604
+ ```
605
+
583
606
  ### PR preview environments
584
607
 
585
608
  The preview duplicates `railway-source-environment` (default `staging`) into `pr-<number>`, points git-connected services at the PR branch, sets `DATABASE_URL` and `DATABASE_URL_DIRECT` to a fresh PlanetScale branch, attaches `pr-<number>.<zone>` with Cloudflare DNS, then sets `BASE_URL`, which triggers the one deployment (run migrations and seeds in your pre-deploy step).
package/dist/args.d.ts CHANGED
@@ -32,47 +32,47 @@ export declare const releaseFlags: z.ZodObject<{
32
32
  profile: z.ZodString;
33
33
  base: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
34
34
  head: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
35
- platform: z.ZodDefault<z.ZodEnum<{
35
+ platform: z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
36
36
  android: "android";
37
37
  ios: "ios";
38
- }>>;
38
+ }>, unknown>>;
39
39
  impactPaths: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string[], string>>;
40
- ignoreExpoImpact: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
40
+ ignoreExpoImpact: z.ZodPipe<z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
41
41
  false: "false";
42
42
  true: "true";
43
- }>>, z.ZodTransform<boolean, "false" | "true">>;
44
- operation: z.ZodDefault<z.ZodEnum<{
43
+ }>, unknown>>, z.ZodTransform<boolean, "false" | "true">>;
44
+ operation: z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
45
45
  deploy: "deploy";
46
46
  "force-native": "force-native";
47
47
  republish: "republish";
48
48
  "retry-ota": "retry-ota";
49
49
  "rollback-embedded": "rollback-embedded";
50
- }>>;
51
- deployType: z.ZodDefault<z.ZodEnum<{
50
+ }>, unknown>>;
51
+ deployType: z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
52
52
  native: "native";
53
53
  none: "none";
54
54
  ota: "ota";
55
- }>>;
55
+ }>, unknown>>;
56
56
  channel: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
57
57
  environment: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
58
- autoSubmit: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
58
+ autoSubmit: z.ZodPipe<z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
59
59
  false: "false";
60
60
  true: "true";
61
- }>>, z.ZodTransform<boolean, "false" | "true">>;
61
+ }>, unknown>>, z.ZodTransform<boolean, "false" | "true">>;
62
62
  preUpdate: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
63
63
  postUpdate: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
64
64
  updateExtraArgs: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
65
- exportSourceMaps: z.ZodPipe<z.ZodDefault<z.ZodEnum<{
65
+ exportSourceMaps: z.ZodPipe<z.ZodDefault<z.ZodPreprocess<z.ZodEnum<{
66
66
  false: "false";
67
67
  true: "true";
68
- }>>, z.ZodTransform<boolean, "false" | "true">>;
68
+ }>, unknown>>, z.ZodTransform<boolean, "false" | "true">>;
69
69
  group: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
70
70
  runtimeVersion: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
71
71
  message: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
72
72
  fingerprint: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string | undefined, string>>>;
73
- dryRun: z.ZodBoolean;
73
+ dryRun: z.ZodPreprocess<z.ZodBoolean, unknown>;
74
74
  }, z.core.$strip>;
75
75
  export type ReleaseFlags = z.output<typeof releaseFlags>;
76
- export declare function toReleaseOptions(command: CommandName, { context: flags, env }: CommandContext<ReleaseFlags>): ReleaseOptions;
76
+ export declare function toReleaseOptions(command: CommandName, { context: flags }: CommandContext<ReleaseFlags>): ReleaseOptions;
77
77
  export declare function needsDecide(operation: Operation): boolean;
78
78
  export declare function shouldIgnoreExpoImpact(options: Pick<ReleaseOptions, 'ignoreExpoImpact' | 'operation'>): boolean;
package/dist/cli.js CHANGED
@@ -2,18 +2,23 @@
2
2
  // @bun
3
3
 
4
4
  // src/cli.ts
5
- import { defineCli, defineCommand } from "hanzio/cli";
5
+ import {
6
+ CliUsageError,
7
+ defineCli,
8
+ defineCommand
9
+ } from "hanzio/cli";
6
10
 
7
11
  // src/args.ts
8
12
  import { z } from "zod";
9
13
  var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
10
14
  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}` });
15
+ var trimmed = (schema) => z.preprocess((value) => typeof value === "string" ? value.trim() : value, schema);
16
+ var trueFalse = (env, fallback) => trimmed(z.enum(["true", "false"])).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
12
17
  var releaseFlags = z.object({
13
18
  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"),
19
+ base: text("BASE").meta({ env: ["EXPO_RELEASE_BASE", "GITHUB_EVENT_BEFORE"] }).describe("Git base SHA for the impact diff"),
20
+ head: text("HEAD").meta({ env: ["EXPO_RELEASE_HEAD", "GITHUB_SHA"] }).describe("Git head SHA for the impact diff"),
21
+ platform: trimmed(z.enum(["ios", "android"])).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
17
22
  impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
18
23
  const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
19
24
  if (paths.length === 0)
@@ -24,14 +29,14 @@ var releaseFlags = z.object({
24
29
  return paths;
25
30
  }).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
26
31
  ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
27
- operation: z.enum([
32
+ operation: trimmed(z.enum([
28
33
  "deploy",
29
34
  "force-native",
30
35
  "retry-ota",
31
36
  "republish",
32
37
  "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"),
38
+ ])).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
39
+ deployType: trimmed(z.enum(["none", "ota", "native"])).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
35
40
  channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
36
41
  environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+)"),
37
42
  autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
@@ -43,7 +48,7 @@ var releaseFlags = z.object({
43
48
  runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
44
49
  message: text("MESSAGE").describe("Override the eas update message"),
45
50
  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")
51
+ dryRun: z.preprocess((value) => typeof value === "string" && /^\s*(true|false)\s*$/.test(value) ? value.trim() === "true" : value, z.boolean()).meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
47
52
  }).superRefine((flags, ctx) => {
48
53
  if (flags.operation === "republish" && !flags.group)
49
54
  ctx.addIssue({
@@ -52,12 +57,10 @@ var releaseFlags = z.object({
52
57
  message: "required by --operation republish"
53
58
  });
54
59
  });
55
- function toReleaseOptions(command, { context: flags, env }) {
60
+ function toReleaseOptions(command, { context: flags }) {
56
61
  return {
57
62
  ...flags,
58
63
  command,
59
- base: flags.base ?? env.GITHUB_EVENT_BEFORE,
60
- head: flags.head ?? env.GITHUB_SHA,
61
64
  channel: flags.channel ?? flags.profile
62
65
  };
63
66
  }
@@ -568,12 +571,20 @@ async function operate(options, decision, ctx) {
568
571
  ctx.stdout.write(`${result}
569
572
  `);
570
573
  }
571
- var COMMANDS = ["release", "decide", "operate"];
572
574
  var cli = defineCli({
573
575
  name: "expo-release",
574
576
  description: "Fingerprint-driven Expo EAS release: OTA update or native store build",
575
577
  globals: releaseFlags,
576
578
  context: (flags) => flags,
579
+ defaultCommand: (env) => env.EXPO_RELEASE_COMMAND?.trim() || undefined,
580
+ onError: (error, io) => {
581
+ if (error instanceof CliUsageError && error.kind === "missing-command" && process.argv.length <= 2) {
582
+ io.stdout.write(error.help);
583
+ return 0;
584
+ }
585
+ const code = io.print();
586
+ return code === 2 ? 1 : code;
587
+ },
577
588
  commands: {
578
589
  release: defineCommand({
579
590
  description: "Run decide, then operate on its decision",
@@ -597,10 +608,4 @@ var cli = defineCli({
597
608
  })
598
609
  }
599
610
  });
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;
604
- }
605
- var code = await cli.run(argv, { env });
606
- process.exitCode = code === 2 ? 1 : code;
611
+ process.exitCode = await cli.run();
package/dist/index.js CHANGED
@@ -3,12 +3,13 @@
3
3
  import { z } from "zod";
4
4
  var DEFAULT_IMPACT_PATHS = ["apps/expo/"];
5
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}` });
6
+ var trimmed = (schema) => z.preprocess((value) => typeof value === "string" ? value.trim() : value, schema);
7
+ var trueFalse = (env, fallback) => trimmed(z.enum(["true", "false"])).default(fallback).transform((value) => value === "true").meta({ env: `EXPO_RELEASE_${env}` });
7
8
  var releaseFlags = z.object({
8
9
  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"),
10
+ base: text("BASE").meta({ env: ["EXPO_RELEASE_BASE", "GITHUB_EVENT_BEFORE"] }).describe("Git base SHA for the impact diff"),
11
+ head: text("HEAD").meta({ env: ["EXPO_RELEASE_HEAD", "GITHUB_SHA"] }).describe("Git head SHA for the impact diff"),
12
+ platform: trimmed(z.enum(["ios", "android"])).default("ios").meta({ env: "EXPO_RELEASE_PLATFORM" }).describe("Native platform"),
12
13
  impactPaths: z.string().default(DEFAULT_IMPACT_PATHS.join(",")).transform((value, ctx) => {
13
14
  const paths = value.split(",").map((path) => path.trim()).filter(Boolean);
14
15
  if (paths.length === 0)
@@ -19,14 +20,14 @@ var releaseFlags = z.object({
19
20
  return paths;
20
21
  }).meta({ env: "EXPO_RELEASE_IMPACT_PATHS" }).describe("Comma-separated repo-root paths that count as an Expo change"),
21
22
  ignoreExpoImpact: trueFalse("IGNORE_EXPO_IMPACT", "false").describe("Check the fingerprint even when no impact path changed"),
22
- operation: z.enum([
23
+ operation: trimmed(z.enum([
23
24
  "deploy",
24
25
  "force-native",
25
26
  "retry-ota",
26
27
  "republish",
27
28
  "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"),
29
+ ])).default("deploy").meta({ env: "EXPO_RELEASE_OPERATION" }).describe("What to run"),
30
+ deployType: trimmed(z.enum(["none", "ota", "native"])).default("none").meta({ env: "EXPO_RELEASE_DEPLOY_TYPE" }).describe("Decision type for operate"),
30
31
  channel: text("CHANNEL").describe("EAS Update channel (default: the profile)"),
31
32
  environment: text("ENVIRONMENT").describe("EAS environment for eas update (required for Expo SDK 55+)"),
32
33
  autoSubmit: trueFalse("AUTO_SUBMIT", "true").describe("Pass --auto-submit on native builds"),
@@ -38,7 +39,7 @@ var releaseFlags = z.object({
38
39
  runtimeVersion: text("RUNTIME_VERSION").describe("Runtime version for rollback-embedded (default: the fingerprint)"),
39
40
  message: text("MESSAGE").describe("Override the eas update message"),
40
41
  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
+ dryRun: z.preprocess((value) => typeof value === "string" && /^\s*(true|false)\s*$/.test(value) ? value.trim() === "true" : value, z.boolean()).meta({ env: "EXPO_RELEASE_DRY_RUN" }).describe("Print mutating EAS commands without running them")
42
43
  }).superRefine((flags, ctx) => {
43
44
  if (flags.operation === "republish" && !flags.group)
44
45
  ctx.addIssue({
@@ -47,12 +48,10 @@ var releaseFlags = z.object({
47
48
  message: "required by --operation republish"
48
49
  });
49
50
  });
50
- function toReleaseOptions(command, { context: flags, env }) {
51
+ function toReleaseOptions(command, { context: flags }) {
51
52
  return {
52
53
  ...flags,
53
54
  command,
54
- base: flags.base ?? env.GITHUB_EVENT_BEFORE,
55
- head: flags.head ?? env.GITHUB_SHA,
56
55
  channel: flags.channel ?? flags.profile
57
56
  };
58
57
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-release",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
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",
@@ -58,7 +58,7 @@
58
58
  "verify:package": "node scripts/verify-package.mjs"
59
59
  },
60
60
  "dependencies": {
61
- "hanzio": "^3.1.0",
61
+ "hanzio": "^3.3.0",
62
62
  "zod": "^4.3.6"
63
63
  },
64
64
  "peerDependencies": {
package/src/args.ts CHANGED
@@ -48,10 +48,16 @@ const text = (env: string) =>
48
48
  .optional()
49
49
  .meta({ env: `EXPO_RELEASE_${env}` })
50
50
 
51
+ // hanzio/cli skips blank env values but keeps padding (`platform: |` in YAML adds a newline).
52
+ const trimmed = <T extends z.ZodType>(schema: T) =>
53
+ z.preprocess(
54
+ (value) => (typeof value === 'string' ? value.trim() : value),
55
+ schema
56
+ )
57
+
51
58
  // `--auto-submit false` takes a value, so these are not boolean switches.
52
59
  const trueFalse = (env: string, fallback: 'true' | 'false') =>
53
- z
54
- .enum(['true', 'false'])
60
+ trimmed(z.enum(['true', 'false']))
55
61
  .default(fallback)
56
62
  .transform((value) => value === 'true')
57
63
  .meta({ env: `EXPO_RELEASE_${env}` })
@@ -64,14 +70,13 @@ export const releaseFlags = z
64
70
  .min(1)
65
71
  .meta({ env: 'EXPO_RELEASE_PROFILE' })
66
72
  .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'])
73
+ base: text('BASE')
74
+ .meta({ env: ['EXPO_RELEASE_BASE', 'GITHUB_EVENT_BEFORE'] })
75
+ .describe('Git base SHA for the impact diff'),
76
+ head: text('HEAD')
77
+ .meta({ env: ['EXPO_RELEASE_HEAD', 'GITHUB_SHA'] })
78
+ .describe('Git head SHA for the impact diff'),
79
+ platform: trimmed(z.enum(['ios', 'android']))
75
80
  .default('ios')
76
81
  .meta({ env: 'EXPO_RELEASE_PLATFORM' })
77
82
  .describe('Native platform'),
@@ -95,19 +100,19 @@ export const releaseFlags = z
95
100
  ignoreExpoImpact: trueFalse('IGNORE_EXPO_IMPACT', 'false').describe(
96
101
  'Check the fingerprint even when no impact path changed'
97
102
  ),
98
- operation: z
99
- .enum([
103
+ operation: trimmed(
104
+ z.enum([
100
105
  'deploy',
101
106
  'force-native',
102
107
  'retry-ota',
103
108
  'republish',
104
109
  'rollback-embedded'
105
110
  ])
111
+ )
106
112
  .default('deploy')
107
113
  .meta({ env: 'EXPO_RELEASE_OPERATION' })
108
114
  .describe('What to run'),
109
- deployType: z
110
- .enum(['none', 'ota', 'native'])
115
+ deployType: trimmed(z.enum(['none', 'ota', 'native']))
111
116
  .default('none')
112
117
  .meta({ env: 'EXPO_RELEASE_DEPLOY_TYPE' })
113
118
  .describe('Decision type for operate'),
@@ -141,7 +146,14 @@ export const releaseFlags = z
141
146
  'Fingerprint hash to use instead of generating one'
142
147
  ),
143
148
  dryRun: z
144
- .boolean()
149
+ .preprocess(
150
+ // hanzio/cli makes booleans of exact `true`/`false`; this takes padded ones too.
151
+ (value) =>
152
+ typeof value === 'string' && /^\s*(true|false)\s*$/.test(value)
153
+ ? value.trim() === 'true'
154
+ : value,
155
+ z.boolean()
156
+ )
145
157
  .meta({ env: 'EXPO_RELEASE_DRY_RUN' })
146
158
  .describe('Print mutating EAS commands without running them')
147
159
  })
@@ -158,13 +170,11 @@ export type ReleaseFlags = z.output<typeof releaseFlags>
158
170
 
159
171
  export function toReleaseOptions(
160
172
  command: CommandName,
161
- { context: flags, env }: CommandContext<ReleaseFlags>
173
+ { context: flags }: CommandContext<ReleaseFlags>
162
174
  ): ReleaseOptions {
163
175
  return {
164
176
  ...flags,
165
177
  command,
166
- base: flags.base ?? env.GITHUB_EVENT_BEFORE,
167
- head: flags.head ?? env.GITHUB_SHA,
168
178
  channel: flags.channel ?? flags.profile
169
179
  }
170
180
  }
package/src/cli.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { type CommandContext, defineCli, defineCommand } from 'hanzio/cli'
4
3
  import {
5
- type CommandName,
4
+ CliUsageError,
5
+ type CommandContext,
6
+ defineCli,
7
+ defineCommand
8
+ } from 'hanzio/cli'
9
+ import {
6
10
  needsDecide,
7
11
  type ReleaseFlags,
8
12
  type ReleaseOptions,
@@ -76,8 +80,6 @@ async function operate(
76
80
  ctx.stdout.write(`${result}\n`)
77
81
  }
78
82
 
79
- const COMMANDS: CommandName[] = ['release', 'decide', 'operate']
80
-
81
83
  const cli = defineCli({
82
84
  name: 'expo-release',
83
85
  description:
@@ -85,6 +87,22 @@ const cli = defineCli({
85
87
  // All three commands take the same flags, before or after the command name.
86
88
  globals: releaseFlags,
87
89
  context: (flags) => flags,
90
+ // The composite action names the command in EXPO_RELEASE_COMMAND and passes no argv.
91
+ defaultCommand: (env) => env.EXPO_RELEASE_COMMAND?.trim() || undefined,
92
+ onError: (error, io) => {
93
+ // With no arguments at all, print help and exit 0.
94
+ if (
95
+ error instanceof CliUsageError &&
96
+ error.kind === 'missing-command' &&
97
+ process.argv.length <= 2
98
+ ) {
99
+ io.stdout.write(error.help)
100
+ return 0
101
+ }
102
+ // Bad arguments exit 1, as before hanzio/cli, which uses 2.
103
+ const code = io.print()
104
+ return code === 2 ? 1 : code
105
+ },
88
106
  commands: {
89
107
  release: defineCommand({
90
108
  description: 'Run decide, then operate on its decision',
@@ -112,23 +130,4 @@ const cli = defineCli({
112
130
  }
113
131
  })
114
132
 
115
- // GitHub Actions passes an unset input as an empty string; treat it as unset.
116
- const env = Object.fromEntries(
117
- Object.entries(process.env).flatMap(([name, value]) =>
118
- value?.trim() ? [[name, value.trim()]] : []
119
- )
120
- )
121
-
122
- let argv = process.argv.slice(2)
123
- if (!argv.some((arg) => COMMANDS.includes(arg as CommandName))) {
124
- // The composite action names the command in EXPO_RELEASE_COMMAND and passes no argv.
125
- argv = env.EXPO_RELEASE_COMMAND
126
- ? [env.EXPO_RELEASE_COMMAND, ...argv]
127
- : argv.length === 0
128
- ? ['--help']
129
- : argv
130
- }
131
-
132
- const code = await cli.run(argv, { env })
133
- // Bad arguments exit 1, as before hanzio/cli, which uses 2.
134
- process.exitCode = code === 2 ? 1 : code
133
+ process.exitCode = await cli.run()