expo-release-it 0.1.2 → 0.1.4

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.
Files changed (75) hide show
  1. package/README.md +133 -12
  2. package/build/command/build.js +135 -60
  3. package/build/command/build.js.map +1 -1
  4. package/build/command/bump.js +15 -18
  5. package/build/command/bump.js.map +1 -1
  6. package/build/command/init.js +13 -9
  7. package/build/command/init.js.map +1 -1
  8. package/build/command/pull.js +136 -50
  9. package/build/command/pull.js.map +1 -1
  10. package/build/command/submit.js +135 -55
  11. package/build/command/submit.js.map +1 -1
  12. package/build/command/upload.js +136 -56
  13. package/build/command/upload.js.map +1 -1
  14. package/build/index.js +194 -141
  15. package/build/index.js.map +1 -1
  16. package/build/util/FastlaneOption.js +10 -7
  17. package/build/util/FastlaneOption.js.map +1 -1
  18. package/build/util/MetadataSyncUtil.d.ts +4 -0
  19. package/build/util/MetadataSyncUtil.js +169 -0
  20. package/build/util/MetadataSyncUtil.js.map +1 -0
  21. package/build/util/OptionHolder.d.ts +2 -1
  22. package/build/util/OptionHolder.js +4 -2
  23. package/build/util/OptionHolder.js.map +1 -1
  24. package/build/util/generateAppStoreConnectApiKeyFile.js +5 -3
  25. package/build/util/generateAppStoreConnectApiKeyFile.js.map +1 -1
  26. package/build/util/injectTemplatePlaceHolders.js +14 -21
  27. package/build/util/injectTemplatePlaceHolders.js.map +1 -1
  28. package/build/util/input/InqueryInputs.d.ts +1 -0
  29. package/build/util/input/InqueryInputs.js +19 -2
  30. package/build/util/input/InqueryInputs.js.map +1 -1
  31. package/build/util/setup/VersionUtil.js +10 -12
  32. package/build/util/setup/VersionUtil.js.map +1 -1
  33. package/build/util/setup/hydrateKeyholderFileValues.js +4 -2
  34. package/build/util/setup/hydrateKeyholderFileValues.js.map +1 -1
  35. package/build/util/setup/prepareAndroid.js +56 -28
  36. package/build/util/setup/prepareAndroid.js.map +1 -1
  37. package/build/util/setup/prepareIos.js +61 -27
  38. package/build/util/setup/prepareIos.js.map +1 -1
  39. package/build/util/setup/setup.js +12 -8
  40. package/build/util/setup/setup.js.map +1 -1
  41. package/package.json +3 -1
  42. package/template/android/fastlane/Fastfile +2 -0
  43. package/template/android/fastlane/Appfile +0 -2
  44. package/template/android/fastlane/metadata/android/ko-KR/changelogs/default.txt +0 -1
  45. package/template/android/fastlane/metadata/android/ko-KR/full_description.txt +0 -0
  46. package/template/android/fastlane/metadata/android/ko-KR/short_description.txt +0 -0
  47. package/template/android/fastlane/metadata/android/ko-KR/title.txt +0 -0
  48. package/template/android/fastlane/metadata/android/ko-KR/video.txt +0 -0
  49. package/template/ios/fastlane/metadata/copyright.txt +0 -0
  50. package/template/ios/fastlane/metadata/ko/apple_tv_privacy_policy.txt +0 -0
  51. package/template/ios/fastlane/metadata/ko/description.txt +0 -0
  52. package/template/ios/fastlane/metadata/ko/keywords.txt +0 -0
  53. package/template/ios/fastlane/metadata/ko/marketing_url.txt +0 -0
  54. package/template/ios/fastlane/metadata/ko/name.txt +0 -0
  55. package/template/ios/fastlane/metadata/ko/privacy_url.txt +0 -0
  56. package/template/ios/fastlane/metadata/ko/promotional_text.txt +0 -0
  57. package/template/ios/fastlane/metadata/ko/release_notes.txt +0 -0
  58. package/template/ios/fastlane/metadata/ko/subtitle.txt +0 -0
  59. package/template/ios/fastlane/metadata/ko/support_url.txt +0 -0
  60. package/template/ios/fastlane/metadata/primary_category.txt +0 -0
  61. package/template/ios/fastlane/metadata/primary_first_sub_category.txt +0 -1
  62. package/template/ios/fastlane/metadata/primary_second_sub_category.txt +0 -1
  63. package/template/ios/fastlane/metadata/review_information/demo_password.txt +0 -0
  64. package/template/ios/fastlane/metadata/review_information/demo_user.txt +0 -0
  65. package/template/ios/fastlane/metadata/review_information/email_address.txt +0 -0
  66. package/template/ios/fastlane/metadata/review_information/first_name.txt +0 -0
  67. package/template/ios/fastlane/metadata/review_information/last_name.txt +0 -0
  68. package/template/ios/fastlane/metadata/review_information/notes.txt +0 -1
  69. package/template/ios/fastlane/metadata/review_information/phone_number.txt +0 -0
  70. package/template/ios/fastlane/metadata/secondary_category.txt +0 -1
  71. package/template/ios/fastlane/metadata/secondary_first_sub_category.txt +0 -1
  72. package/template/ios/fastlane/metadata/secondary_second_sub_category.txt +0 -1
  73. package/template/ios/fastlane/screenshots/README.txt +0 -30
  74. package/template/key/android_upload_certificate.pem +0 -2
  75. /package/template/key/{ios_testflight_upload_api_key.p8 → ios_app_store_connect_api_key.p8} +0 -0
package/README.md CHANGED
@@ -2,22 +2,28 @@
2
2
 
3
3
  ![expo-release-it thumbnail](./asset/social-image.png)
4
4
 
5
- Opinioned Expo CICD workflow CLI for building & uploading & submitting Expo CNG project on your machine
5
+ Opinionated Expo CICD workflow CLI for building & uploading & submitting Expo projects on your machine
6
6
 
7
7
  > [!CAUTION]
8
- > This project is under developing.
8
+ > This project is under development.
9
9
 
10
10
  # Features
11
11
 
12
- - No EAS
13
- - Manage Keys, Credentials, Metadatas in a simple way
14
- - Working with CNG mode (also available with non CNG environment)
15
- - Build artifacts locally (`apk`, `aab`, `ipa`)
16
- - Upload artifacts to Google Play Console Internal Testing Track & iOS Testflight
17
- - Submit releases for review
12
+ - 🎉 No **EAS**
13
+ - 🩵 No headaches when understanding and managing **keys and credentials**
14
+ - 🧩 Manage and sync **store metadata** easily
15
+ - 🌍 Works with [Expo CNG](https://docs.expo.dev/workflow/continuous-native-generation/) mode (also available in non-CNG environments)
16
+ - 📦 Build artifacts **locally** (`apk`, `aab`, `ipa`)
17
+ - 🚀 Upload artifacts to Google Play Console Internal Testing Track and iOS TestFlight
18
+ - ✅ Submit releases for review
18
19
 
19
20
  # Why?
20
21
 
22
+ **You don't need Expo EAS and to understand how it works in most small projects and our machine is much faster than using the cloud.**
23
+
24
+ <details>
25
+ <summary>Full Details</summary>
26
+
21
27
  Expo EAS is convenient, but it requires additional configuration files, console setup, and a conceptual understanding of Over-The-Air (OTA) updates. Also, builds must be performed on EAS Cloud, which is not free and can be slow.
22
28
 
23
29
  While EAS is useful for larger teams, if you are developing alone or with just a few people and question the need for such a process, `expo-release-it` is the answer.
@@ -26,32 +32,144 @@ Based on years of experience developing with React Native, I encourage a more tr
26
32
 
27
33
  This approach is much faster, simpler, and, most importantly, allows you to take full advantage of your computer’s fast CPU.
28
34
 
35
+ OTA updates are a powerful tool in React Native, but to fully take advantage of them, you need a self-hosted CDN server, a version management console both in the cloud and inside the app, and you have to separately manage binary versions and bundle versions—all of which introduce significant complexity and semantic overhead that often outweigh the benefits of implementing OTA updates. Furthermore, due to a general lack of deep understanding about OTA updates, issues like native library version mismatches can occur frequently, so OTA updates are excluded by default.
36
+
37
+ I’m also a big fan of OTA updates, and I’ve enjoyed a lot of efficiency when working in larger teams, but it always required setting up many supporting systems in advance.
38
+
29
39
  `expo-release-it` is a CLI tool that encapsulates the know-how I have gained over years of automating build and review processes.
30
40
  Internally, it ports and integrates tools like Fastlane and Match to automate native binary builds and review requests.
31
41
 
32
42
  Therefore, this tool enforces an opinionated versioning, build, and review request workflow.
33
43
  However, once you understand this approach, your app deployment process will become significantly faster.
34
44
 
45
+ </details>
35
46
 
36
- # Requirements
47
+ # Structurual Requirements
37
48
 
38
- - Match iOS, Android Binary build version like `1.0.0`
39
- - All Keys, Credentials(except iOS certificates & provisioning profiles) are stored in VCS
49
+ - `Version Code(Android)` and `Builder Number(iOS)` are the same.
50
+ - All Keys, Credentials(except iOS certificates & provisioning profiles served in Github separately) are stored in VCS
40
51
 
41
52
  # Getting Started
42
53
 
54
+ ## 1. Ruby, Bundler
55
+
56
+ `expo-release-it` requires `bundler` of ruby is available in your environment to run `Fastlane` internally.
57
+
58
+ If not, set up your environment so that `ruby` and `bundler` are available by following [Ruby Install Guide](https://www.ruby-lang.org/en/downloads/)
59
+
60
+ ## 2. Prepare Keys & Credentials
61
+
62
+ `init` command generates sample key files structure in `expo-release-it/key`. They are **noob files** and you must replace file contents.
63
+
64
+ > [!IMPORTANT]
65
+ **You must prepare all required key files in `expo-release-it/key` exactly the same filenames.**
66
+
67
+ ### `android_play_console_service_account.json`
68
+
69
+ Google Play Console actions require authentication using Google Service Account.
70
+
71
+ You can issue new Google Play Service Account by following Guide
72
+ and prepare `json` file.
73
+
74
+ - [How to Get Your Google Play JSON Key](https://help.radio.co/en/articles/6232140-how-to-get-your-google-play-json-key)
75
+
76
+ ### `android_release.keystore`
77
+
78
+ Prepare your android release keystore file for **uploading** and **signing** Android build artifacts.
79
+
80
+ Additional passwords, alias will be prompted during the `init` command and be stored in `keyholder.json`.
81
+
82
+ - [Keystore generation with Android Studio](https://developer.android.com/studio/publish/app-signing.html#generate-key)
83
+ - [Keystore generation with CLI](https://gist.github.com/henriquemenezes/70feb8fff20a19a65346e48786bedb8f)
84
+
85
+ ### `ios_app_store_connect_api_key`
86
+
87
+ App Store Connect actions require authentication using App Store Connect Api Key(.p8)
88
+
89
+ Additional `issuer_id`, `key_id` values will be prompted during the `init` command and be stored in `keyholder.json`
90
+
91
+ - [Guide](https://www.revenuecat.com/docs/service-credentials/itunesconnect-app-specific-shared-secret/app-store-connect-api-key-configuration)
92
+ - [Apple Docs Guide](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api)
93
+
94
+ ## 3. Create & Configure app on the stores as much as possible
95
+
96
+ Create and configure your application in Google Play Console & App Store Connect as many as possible.
43
97
 
98
+ You'd have to fill your key & credentials information using `init` command and pull store metadatas using `pull` command later.
99
+
100
+ ## 4. Set binary version in expo config file.
101
+
102
+ The following buildNumber, versionCode format `ABBBCCC` is not mandatory but recommended for readability.
103
+
104
+ If your expo config file is `app.json`, then set like the following.
105
+
106
+ ```json
107
+ {
108
+ "expo": {
109
+ "version": "1.2.3",
110
+ "ios": {
111
+ "buildNumber": "1002003",
112
+ },
113
+ "android": {
114
+ "versionCode": 1002003,
115
+ },
116
+ }
117
+ }
118
+
119
+ ```
120
+
121
+ If your expo config file is `app.config.(js|ts|mjs|cjs)`, then set like the following.
122
+
123
+ ```typescript
124
+ import type { ExpoConfig, ConfigContext } from 'expo/config';
125
+ const VERSION_NAME = '1.2.3'; // VERSION_NAME variable is required.
126
+ const VERSION_CODE = 1002003; // VERSION_CODE variable is required.
127
+
128
+ export default ({ config }: ConfigContext): ExpoConfig => {
129
+ return {
130
+ ...config,
131
+ version: VERSION_NAME,
132
+ ios: {
133
+ buildNumber: VERSION_CODE + '',
134
+ },
135
+ android: {
136
+ versionCode: VERSION_CODE,
137
+ },
138
+ };
139
+ };
140
+
141
+ ```
44
142
 
45
143
  # Usage
46
144
 
47
145
  ```shell
48
- npx expo-release-it (init|bump|build|upload|submit) [options]
146
+ npx expo-release-it (init|bump|pull|build|upload|submit) [options]
49
147
  ```
50
148
 
149
+ All commands are powered by [Commander.js](https://github.com/tj/commander.js?) means you can query all available options with `--help` or `-h`.
150
+
51
151
  ## init
52
152
 
153
+ **Configure resources and environment**
154
+
155
+ ```shell
156
+ npx expo-release-it init
157
+ ```
158
+
159
+ The `init` command asks you for many form fields, and the information is saved in `expo-release-it/keyholder.json` in your project.
160
+
161
+ Even after running `init`, you can manually edit `keyholder.json`, and all subsequent commands will use the updated values.
162
+
53
163
  ## bump
54
164
 
165
+ **Bump binary release patch version with modifying app.json**
166
+
167
+ ```shell
168
+ npx expo-release-it bump
169
+ ```
170
+
171
+ ## pull
172
+
55
173
  ## build
56
174
 
57
175
  ## upload
@@ -60,6 +178,9 @@ npx expo-release-it (init|bump|build|upload|submit) [options]
60
178
 
61
179
  # Limitation
62
180
 
181
+ # Troubleshooting
182
+
183
+ # Licenses
63
184
 
64
185
  - See [LICENSE](/LICENSE)
65
186
 
@@ -3,26 +3,26 @@ import fs from "fs-extra";
3
3
  import * as path from "path";
4
4
  var join2 = path.join;
5
5
  var resolve2 = path.resolve;
6
- function exist(path3) {
7
- return fs.existsSync(path3);
6
+ function exist(path2) {
7
+ return fs.existsSync(path2);
8
8
  }
9
- function isDir(path3) {
10
- return exist(path3) && fs.lstatSync(path3).isDirectory();
9
+ function isDir(path2) {
10
+ return exist(path2) && fs.lstatSync(path2).isDirectory();
11
11
  }
12
12
  function relativePath(from, to) {
13
13
  return path.relative(from, to);
14
14
  }
15
- function isFile(path3) {
16
- return exist(path3) && fs.lstatSync(path3).isFile();
15
+ function isFile(path2) {
16
+ return exist(path2) && fs.lstatSync(path2).isFile();
17
17
  }
18
- function read(path3) {
19
- return fs.readFileSync(path3, { encoding: "utf8" });
18
+ function read(path2) {
19
+ return fs.readFileSync(path2, { encoding: "utf8" });
20
20
  }
21
- function readdir(path3) {
22
- return fs.readdirSync(path3);
21
+ function readdir(path2) {
22
+ return fs.readdirSync(path2);
23
23
  }
24
- function readJsonSlow(path3) {
25
- return fs.readJSONSync(path3);
24
+ function readJsonSlow(path2) {
25
+ return fs.readJSONSync(path2);
26
26
  }
27
27
  function write(p, content) {
28
28
  const dir = path.dirname(p);
@@ -34,21 +34,21 @@ function write(p, content) {
34
34
  function copy(src, dest, options) {
35
35
  fs.copySync(src, dest, options);
36
36
  }
37
- function remove(path3) {
38
- if (!exist(path3)) {
37
+ function remove(path2) {
38
+ if (!exist(path2)) {
39
39
  return;
40
40
  }
41
- if (fs.lstatSync(path3).isDirectory()) {
42
- return fs.rmSync(path3, { force: true, recursive: true });
41
+ if (fs.lstatSync(path2).isDirectory()) {
42
+ return fs.rmSync(path2, { force: true, recursive: true });
43
43
  } else {
44
- return fs.rmSync(path3, { force: true });
44
+ return fs.rmSync(path2, { force: true });
45
45
  }
46
46
  }
47
- async function iterateDir(path3, fn) {
48
- if (!isDir(path3)) {
47
+ async function iterateDir(path2, fn) {
48
+ if (!isDir(path2)) {
49
49
  return;
50
50
  }
51
- for (const file of fs.readdirSync(path3)) {
51
+ for (const file of fs.readdirSync(path2)) {
52
52
  await fn(file);
53
53
  }
54
54
  }
@@ -58,8 +58,10 @@ var package_default = {
58
58
  name: "expo-release-it",
59
59
  module: "src/index.ts",
60
60
  type: "module",
61
- version: "0.1.1",
61
+ version: "0.1.3",
62
62
  license: "MIT",
63
+ homepage: "https://github.com/mym0404/expo-release-it",
64
+ repository: "https://github.com/mym0404/expo-release-it",
63
65
  scripts: {
64
66
  build: "tsup",
65
67
  dev: "yarn run build && NODE_ENV=development node bin/expo-release-it.mjs",
@@ -115,7 +117,7 @@ var OptionHolder = {
115
117
  keyholderMap: {},
116
118
  templateDirNames: ["key"],
117
119
  projectDir: "",
118
- outputOfInitDir: "",
120
+ resourcesDir: "",
119
121
  keyDir: "",
120
122
  keyholderFilePath: "",
121
123
  versionName: "",
@@ -139,18 +141,12 @@ async function iterateAllFiles(root, callback) {
139
141
  }
140
142
 
141
143
  // src/util/injectTemplatePlaceHolders.ts
142
- import * as path2 from "node:path";
143
144
  async function injectTemplatePlaceHolders(root) {
144
145
  await iterateAllFiles(root, async (filePath) => {
145
- const dirPath = path2.dirname(filePath);
146
146
  let content = read(filePath);
147
147
  for (const [key, value] of Object.entries(OptionHolder.keyholderMap)) {
148
148
  content = content.replaceAll(`{{${key}}}`, value);
149
149
  }
150
- content = content.replaceAll(
151
- "{{key_dir_relative_path}}",
152
- relativePath(dirPath, OptionHolder.keyDir)
153
- );
154
150
  write(filePath, content);
155
151
  });
156
152
  }
@@ -194,6 +190,71 @@ ${msg.join(", ")}`)),
194
190
  dim: (...msg) => console.log(chalk2.dim(msg.join(", ")))
195
191
  };
196
192
 
193
+ // src/util/MetadataSyncUtil.ts
194
+ import chalk3 from "chalk";
195
+ function copyIosMetadata(to) {
196
+ const iosDir = join2(OptionHolder.projectDir, "ios");
197
+ const nativeDir = join2(iosDir, "fastlane");
198
+ const metadataNativeDir = join2(nativeDir, "metadata");
199
+ const screenshotsNativeDir = join2(nativeDir, "screenshots");
200
+ const resourcesDir = join2(OptionHolder.resourcesDir, "metadata/ios");
201
+ const metadataResourcesDir = join2(resourcesDir, "metadata");
202
+ const screenshotsResourcesDir = join2(resourcesDir, "screenshots");
203
+ if (to === "resources") {
204
+ if (exist(metadataNativeDir) && exist(screenshotsNativeDir)) {
205
+ copy(metadataNativeDir, metadataResourcesDir);
206
+ copy(screenshotsNativeDir, screenshotsResourcesDir);
207
+ logger.done(
208
+ `iOS Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
209
+ );
210
+ } else {
211
+ logger.warn(
212
+ `iOS Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
213
+ );
214
+ }
215
+ } else {
216
+ if (exist(metadataResourcesDir) && exist(screenshotsResourcesDir)) {
217
+ copy(metadataResourcesDir, metadataNativeDir);
218
+ copy(screenshotsResourcesDir, screenshotsNativeDir);
219
+ logger.done(
220
+ `iOS Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
221
+ );
222
+ } else {
223
+ logger.warn(
224
+ `iOS Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}`
225
+ );
226
+ }
227
+ }
228
+ }
229
+ function copyAndroidMetadata(to) {
230
+ const androidDir = join2(OptionHolder.projectDir, "android");
231
+ const nativeDir = join2(androidDir, "fastlane/metadata/android");
232
+ const resourcesDir = join2(OptionHolder.resourcesDir, "metadata/android");
233
+ if (to === "resouces") {
234
+ if (exist(nativeDir)) {
235
+ copy(nativeDir, resourcesDir);
236
+ logger.done(
237
+ `Android Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
238
+ );
239
+ } else {
240
+ logger.warn(
241
+ `Android Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
242
+ );
243
+ }
244
+ } else {
245
+ if (exist(resourcesDir)) {
246
+ copy(resourcesDir, nativeDir);
247
+ logger.done(
248
+ `Android Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
249
+ );
250
+ } else {
251
+ logger.warn(
252
+ `Android Metadata not found in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}`
253
+ );
254
+ }
255
+ }
256
+ }
257
+
197
258
  // src/util/setup/prepareAndroid.ts
198
259
  async function prepareAndroid() {
199
260
  const srcDir = resolve2(OptionHolder.cli.templateDir, "android");
@@ -215,8 +276,9 @@ async function prepareAndroid() {
215
276
  "Hydrate template placeholders",
216
277
  injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
217
278
  );
218
- await exe({ cwd: destDir })`bundle install`;
279
+ await exe("bundle", ["install"], { cwd: destDir });
219
280
  logger.success("Bunder Install - Done");
281
+ copyAndroidMetadata("native");
220
282
  }
221
283
  async function replaceAndroidSigningConfig(destDir) {
222
284
  const buildGradleDirPath = resolve2(destDir, "app");
@@ -231,7 +293,7 @@ async function replaceAndroidSigningConfig(destDir) {
231
293
  `
232
294
  signingConfigs {
233
295
  debug {
234
- storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.outputOfInitDir, "key", "android_release_keystore.jks"))}')
296
+ storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.resourcesDir, "key", "android_release.keystore"))}')
235
297
  storePassword '${android_keystore_store_password}'
236
298
  keyAlias '${android_keystore_key_alias}'
237
299
  keyPassword '${android_keystore_key_password}'
@@ -261,6 +323,8 @@ function throwError(message, e) {
261
323
 
262
324
  // src/util/setup/VersionUtil.ts
263
325
  import semver from "semver";
326
+ var versionNameRegex = () => /const\s+VERSION_NAME\s*?=\s*?['"]([\d\.]*?)['"]\s*?;?/;
327
+ var versionCodeRegex = () => /const\s+VERSION_CODE\s*?=\s*?(\d+)\s*?;?/;
264
328
  async function parseBinaryVersions() {
265
329
  const files = readdir(OptionHolder.projectDir);
266
330
  const jsConfigFiles = ["app.config.ts", "app.config.js", "app.config.cjs", "app.config.mjs"];
@@ -279,8 +343,8 @@ async function parseBinaryVersions() {
279
343
  async function parseFromJsConfigFile(filename) {
280
344
  const filePath = resolve2(OptionHolder.projectDir, filename);
281
345
  const content = read(filePath);
282
- const VERSION_NAME = /const VERSION_NAME = '(.*?)';/.exec(content)?.[1];
283
- const VERSION_CODE = /const VERSION_CODE = (.*?);/.exec(content)?.[1];
346
+ const VERSION_NAME = versionNameRegex().exec(content)?.[1];
347
+ const VERSION_CODE = versionCodeRegex().exec(content)?.[1];
284
348
  if (!VERSION_NAME) {
285
349
  throwError("cannot parse version name");
286
350
  }
@@ -328,10 +392,10 @@ async function setup() {
328
392
  }
329
393
  async function setupBasicOptions() {
330
394
  OptionHolder.projectDir = isDev ? resolve2("example") : resolve2(".");
331
- OptionHolder.outputOfInitDir = resolve2(OptionHolder.projectDir, "expo-release-it");
332
- OptionHolder.keyDir = resolve2(OptionHolder.outputOfInitDir, "key");
333
- OptionHolder.keyholderFilePath = resolve2(OptionHolder.outputOfInitDir, "keyholder.json");
334
- OptionHolder.tempDir = resolve2(OptionHolder.outputOfInitDir, ".temp");
395
+ OptionHolder.resourcesDir = resolve2(OptionHolder.projectDir, "expo-release-it");
396
+ OptionHolder.keyDir = resolve2(OptionHolder.resourcesDir, "key");
397
+ OptionHolder.keyholderFilePath = resolve2(OptionHolder.resourcesDir, "keyholder.json");
398
+ OptionHolder.tempDir = resolve2(OptionHolder.resourcesDir, ".temp");
335
399
  }
336
400
 
337
401
  // src/util/setup/prepareIos.ts
@@ -354,12 +418,13 @@ async function prepareIos() {
354
418
  "Hydrate template placeholders",
355
419
  injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
356
420
  );
357
- await exe({ cwd: destDir })`bundle install`;
421
+ await exe("bundle", ["install"], { cwd: destDir });
358
422
  logger.success("Bunder Install - Done");
423
+ copyIosMetadata("native");
359
424
  }
360
425
 
361
426
  // src/command/build.ts
362
- import chalk4 from "chalk";
427
+ import chalk5 from "chalk";
363
428
 
364
429
  // src/util/calculateElapsed.ts
365
430
  import { padZero } from "@mj-studio/js-util";
@@ -373,7 +438,7 @@ function calculateElapsed(t) {
373
438
  // src/util/input/InqueryInputs.ts
374
439
  import { select } from "@inquirer/prompts";
375
440
  import { is } from "@mj-studio/js-util";
376
- import chalk3 from "chalk";
441
+ import chalk4 from "chalk";
377
442
  var InqueryInputs = {
378
443
  platform: async () => {
379
444
  if (!OptionHolder.input.platform) {
@@ -389,6 +454,21 @@ var InqueryInputs = {
389
454
  });
390
455
  }
391
456
  },
457
+ platformAll: async () => {
458
+ if (!OptionHolder.input.platformAll) {
459
+ OptionHolder.input.platformAll = await select({
460
+ message: constructInquirerFormattedMessage({
461
+ name: "Platform",
462
+ explanation: `Platforms to run action`
463
+ }),
464
+ choices: [
465
+ { name: "all", value: "all", description: "Both" },
466
+ { name: "ios", value: "ios", description: "Release ios" },
467
+ { name: "android", value: "android", description: "Release android" }
468
+ ]
469
+ });
470
+ }
471
+ },
392
472
  podinstall: async () => {
393
473
  if (OptionHolder.input.platform === "ios") {
394
474
  OptionHolder.input.pod = await select({
@@ -437,19 +517,19 @@ function constructInquirerFormattedMessage({
437
517
  referUrl,
438
518
  name
439
519
  }) {
440
- let ret = `${chalk3.bold(name)}`;
520
+ let ret = `${chalk4.bold(name)}`;
441
521
  if (explanation) {
442
522
  ret += `
443
523
 
444
- ${chalk3.italic.blue(explanation)}`;
524
+ ${chalk4.italic.blue(explanation)}`;
445
525
  }
446
526
  if (example) {
447
527
  ret += `
448
- ${chalk3.italic(`${chalk3.greenBright("example :")} ${example}`)}`;
528
+ ${chalk4.italic(`${chalk4.greenBright("example :")} ${example}`)}`;
449
529
  }
450
530
  if (referUrl) {
451
531
  ret += `
452
- ${chalk3.italic(`${chalk3.greenBright("references :")} ${chalk3.underline.blue(referUrl)}`)}`;
532
+ ${chalk4.italic(`${chalk4.greenBright("references :")} ${chalk4.underline.blue(referUrl)}`)}`;
453
533
  }
454
534
  if (explanation) {
455
535
  ret += "\n";
@@ -466,10 +546,10 @@ function getIosFastlaneOptions() {
466
546
  ...Object.entries(OptionHolder.keyholderMap).map(([k, v]) => `${k}:${v}`),
467
547
  `version_name:${OptionHolder.versionName}`,
468
548
  `version_code:${OptionHolder.versionCode}`,
469
- `api_key_filepath:${resolve2(OptionHolder.keyDir, "ios_testflight_upload_api_key.p8")}`,
549
+ `api_key_filepath:${resolve2(OptionHolder.keyDir, "ios_app_store_connect_api_key.p8")}`,
470
550
  `upload_metadata:${OptionHolder.input.uploadMetadata}`,
471
- `ipa_path:${resolve2(OptionHolder.outputOfInitDir, "output", "ios", "app.ipa")}`,
472
- `ipa_dir:${resolve2(OptionHolder.outputOfInitDir, "output", "ios")}`,
551
+ `ipa_path:${resolve2(OptionHolder.resourcesDir, "output", "ios", "app.ipa")}`,
552
+ `ipa_dir:${resolve2(OptionHolder.resourcesDir, "output", "ios")}`,
473
553
  `ipa_name:app.ipa`
474
554
  ];
475
555
  }
@@ -481,13 +561,12 @@ async function build({ options }) {
481
561
  await setup();
482
562
  await promptInputs();
483
563
  logger.info("Build Started");
484
- logger.info(`Version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
485
564
  if (OptionHolder.input.platform === "ios") {
486
565
  await buildIos();
487
- logger.success(`iOS Build ${chalk4.bold.inverse(calculateElapsed(startTime))}`);
566
+ logger.success(`iOS Build ${chalk5.bold.inverse(calculateElapsed(startTime))}`);
488
567
  } else {
489
568
  await buildAndroid();
490
- logger.success(`Android Build ${chalk4.bold.inverse(calculateElapsed(startTime))}`);
569
+ logger.success(`Android Build ${chalk5.bold.inverse(calculateElapsed(startTime))}`);
491
570
  }
492
571
  }
493
572
  async function promptInputs() {
@@ -513,7 +592,7 @@ async function buildIos() {
513
592
  remove(resolve2(iosDir, ".xcode.env.local"));
514
593
  await exeEnv("bundle", ["exec", "fastlane", "build", ...getIosFastlaneOptions()]);
515
594
  logger.success(
516
- `artifact generated: ${relativePath(OptionHolder.projectDir, resolve2(OptionHolder.outputOfInitDir, "output", "ios", "app.ipa"))}`
595
+ `artifact generated: ${relativePath(OptionHolder.projectDir, resolve2(OptionHolder.resourcesDir, "output", "ios", "app.ipa"))}`
517
596
  );
518
597
  }
519
598
  }
@@ -528,19 +607,15 @@ async function buildAndroid() {
528
607
  let buildOutputDir;
529
608
  let outputDir;
530
609
  if (OptionHolder.input.androidOutput === "aab") {
531
- await spinner(
532
- "Bundle AAB",
533
- exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:bundleRelease"])
534
- );
610
+ await exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:bundleRelease"]);
611
+ logger.done("Bundle AAB");
535
612
  buildOutputDir = resolve2(androidDir, "app", "build", "outputs", "bundle", "release");
536
- outputDir = resolve2(OptionHolder.outputOfInitDir, "output", "android", "aab");
613
+ outputDir = resolve2(OptionHolder.resourcesDir, "output", "android", "aab");
537
614
  } else {
538
- await spinner(
539
- "Bundle APK",
540
- exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:assembleRelease"])
541
- );
615
+ await exeEnv(isWin ? "gradle.bat" : "./gradlew", ["app:assembleRelease"]);
616
+ logger.done("Assemble APK");
542
617
  buildOutputDir = resolve2(androidDir, "app", "build", "outputs", "apk", "release");
543
- outputDir = resolve2(OptionHolder.outputOfInitDir, "output", "android", "apk");
618
+ outputDir = resolve2(OptionHolder.resourcesDir, "output", "android", "apk");
544
619
  }
545
620
  remove(outputDir);
546
621
  copy(buildOutputDir, outputDir);