expo-release-it 0.1.3 → 0.1.5

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 (43) hide show
  1. package/README.md +205 -13
  2. package/build/command/build.js +68 -26
  3. package/build/command/build.js.map +1 -1
  4. package/build/command/bump.js +10 -13
  5. package/build/command/bump.js.map +1 -1
  6. package/build/command/init.js +7 -3
  7. package/build/command/init.js.map +1 -1
  8. package/build/command/pull.js +68 -25
  9. package/build/command/pull.js.map +1 -1
  10. package/build/command/submit.js +72 -30
  11. package/build/command/submit.js.map +1 -1
  12. package/build/command/upload.js +73 -31
  13. package/build/command/upload.js.map +1 -1
  14. package/build/index.js +104 -91
  15. package/build/index.js.map +1 -1
  16. package/build/util/FastlaneOption.js +4 -2
  17. package/build/util/FastlaneOption.js.map +1 -1
  18. package/build/util/MetadataSyncUtil.js +48 -19
  19. package/build/util/MetadataSyncUtil.js.map +1 -1
  20. package/build/util/OptionHolder.d.ts +1 -0
  21. package/build/util/OptionHolder.js +3 -1
  22. package/build/util/OptionHolder.js.map +1 -1
  23. package/build/util/generateAppStoreConnectApiKeyFile.js +4 -2
  24. package/build/util/generateAppStoreConnectApiKeyFile.js.map +1 -1
  25. package/build/util/injectTemplatePlaceHolders.js +3 -1
  26. package/build/util/injectTemplatePlaceHolders.js.map +1 -1
  27. package/build/util/input/InqueryInputs.d.ts +1 -0
  28. package/build/util/input/InqueryInputs.js +18 -1
  29. package/build/util/input/InqueryInputs.js.map +1 -1
  30. package/build/util/setup/VersionUtil.js +9 -11
  31. package/build/util/setup/VersionUtil.js.map +1 -1
  32. package/build/util/setup/hydrateKeyholderFileValues.js +3 -1
  33. package/build/util/setup/hydrateKeyholderFileValues.js.map +1 -1
  34. package/build/util/setup/prepareAndroid.js +25 -11
  35. package/build/util/setup/prepareAndroid.js.map +1 -1
  36. package/build/util/setup/prepareIos.js +26 -12
  37. package/build/util/setup/prepareIos.js.map +1 -1
  38. package/build/util/setup/setup.js +7 -3
  39. package/build/util/setup/setup.js.map +1 -1
  40. package/package.json +3 -1
  41. package/template/key/android_upload_certificate.pem +0 -2
  42. /package/template/key/{android_release_keystore.jks → android_release.keystore} +0 -0
  43. /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,42 +32,228 @@ 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.
97
+
98
+ You'd have to fill your key & credentials information using `init` command and pull store metadatas using `pull` command later.
99
+
100
+ You would need to upload & publish your first artifact, release manually at first.
101
+
102
+ ## 4. Set binary version in expo config file.
103
+
104
+ The following buildNumber, versionCode format `ABBBCCC` is not mandatory but recommended for readability.
105
+
106
+ If your expo config file is `app.json`, then set like the following.
107
+
108
+ ```json
109
+ {
110
+ "expo": {
111
+ "version": "1.2.3",
112
+ "ios": {
113
+ "buildNumber": "1002003",
114
+ },
115
+ "android": {
116
+ "versionCode": 1002003,
117
+ },
118
+ }
119
+ }
120
+
121
+ ```
122
+
123
+ If your expo config file is `app.config.(js|ts|mjs|cjs)`, then set like the following.
124
+
125
+ ```typescript
126
+ import type { ExpoConfig, ConfigContext } from 'expo/config';
127
+ const VERSION_NAME = '1.2.3'; // VERSION_NAME variable is required.
128
+ const VERSION_CODE = 1002003; // VERSION_CODE variable is required.
129
+
130
+ export default ({ config }: ConfigContext): ExpoConfig => {
131
+ return {
132
+ ...config,
133
+ version: VERSION_NAME,
134
+ ios: {
135
+ buildNumber: VERSION_CODE + '',
136
+ },
137
+ android: {
138
+ versionCode: VERSION_CODE,
139
+ },
140
+ };
141
+ };
142
+
143
+ ```
144
+
145
+ ## 5. Update `.gitignore`
146
+
147
+ ```gitignore
148
+ # Expo Release It
149
+
150
+ expo-release-it/.temp
151
+ android/vendor/bundle
152
+ ios/vendor/bundle
153
+ !*.jks
154
+ !*.p8
155
+ !*.keystore
156
+ ```
43
157
 
158
+ ## 6. Run `expo-release-it init`
44
159
 
45
160
  # Usage
46
161
 
47
162
  ```shell
48
- npx expo-release-it (init|bump|build|upload|submit) [options]
163
+ npx expo-release-it (init|bump|pull|build|upload|submit) [options]
49
164
  ```
50
165
 
166
+ All commands are powered by [Commander.js](https://github.com/tj/commander.js?) means you can query all available options with `--help` or `-h`.
167
+
51
168
  ## init
52
169
 
170
+ **Configure resources and environment**
171
+
172
+ ```shell
173
+ npx expo-release-it init
174
+ ```
175
+
176
+ The `init` command asks you for many form fields, and the information is saved in `expo-release-it/keyholder.json` in your project.
177
+
178
+ Even after running `init`, you can manually edit `keyholder.json`, and all subsequent commands will use the updated values.
179
+
180
+ The resources & output directory structure is the following.
181
+
182
+ ```text
183
+ 📁 expo-release-it/
184
+ ├── 📁 key/ # key & certificate files
185
+ ├── 📁 metadata/ # result of `pull`
186
+ │ ├── 📁 android/ # android store metadatas
187
+ │ └── 📁 ios/ # ios store metadatas
188
+ ├── 📁 output/ # result of `build`
189
+ │ ├── 📁 android/ # android build output
190
+ │ └── 📁 ios/ # ios build output
191
+ └── keyholder.json
192
+ ```
193
+
53
194
  ## bump
54
195
 
196
+ **Bump binary release patch version with modifying expo config file**
197
+
198
+ ```shell
199
+ npx expo-release-it bump
200
+ ```
201
+
55
202
  ## pull
56
203
 
204
+ **Download metadatas from stores**
205
+
206
+ ```shell
207
+ npx expo-release-it pull
208
+ ```
209
+
210
+ The downloaded metadatas are stored in `expo-release-it/metadata`.
211
+
212
+ Check the following out to configure metadatas.
213
+
214
+ - [Android Metadata Manage Guide](https://docs.fastlane.tools/actions/supply/)
215
+ - [iOS Metadata Manage Guide](https://docs.fastlane.tools/actions/deliver/)
216
+
217
+
57
218
  ## build
58
219
 
220
+ **Build artifacts (apk, aab, ipa)**
221
+
222
+ ```shell
223
+ npx expo-release-it build
224
+ ```
225
+
59
226
  ## upload
60
227
 
228
+ **Upload artifact to play console internal test track(android) and testflight(ios)**
229
+
230
+ ```shell
231
+ npx expo-release-it upload
232
+ ```
233
+
61
234
  ## submit
62
235
 
63
- # Limitation
236
+ **Submit Google Play Console & App Store Connect Review with latest testing track**
237
+
238
+ ```shell
239
+ npx expo-release-it submit
240
+ ```
241
+
242
+ # About Security
243
+
244
+ That’s right. You might be wondering whether storing files like .p8, keystore files, or key values in version control is truly secure.
245
+ In conclusion, if you’re using a private repository, you can consider it to be somewhat safe.
246
+
247
+ For example, Fastlane Match also uses OpenSSL encryption, but it still stores certificates and profiles in a private Git repository.
248
+
249
+ While you could implement additional security policies, I haven’t taken any extra measures for now.
250
+ If you need further encryption, you can build middleware in your pipeline that runs expo-release-it so that everything in the expo-release-it directory goes through encryption and decryption processes.
251
+
252
+ Any suggestions are welcome.
253
+
254
+ # Troubleshooting
64
255
 
256
+ # Licenses
65
257
 
66
258
  - See [LICENSE](/LICENSE)
67
259
 
@@ -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.2",
61
+ version: "0.1.4",
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",
@@ -199,17 +201,29 @@ function copyIosMetadata(to) {
199
201
  const metadataResourcesDir = join2(resourcesDir, "metadata");
200
202
  const screenshotsResourcesDir = join2(resourcesDir, "screenshots");
201
203
  if (to === "resources") {
202
- copy(metadataNativeDir, metadataResourcesDir);
203
- copy(screenshotsNativeDir, screenshotsResourcesDir);
204
- logger.done(
205
- `iOS Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
206
- );
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
+ }
207
215
  } else {
208
- copy(metadataResourcesDir, metadataNativeDir);
209
- copy(screenshotsResourcesDir, screenshotsNativeDir);
210
- logger.done(
211
- `iOS Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
212
- );
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
+ }
213
227
  }
214
228
  }
215
229
  function copyAndroidMetadata(to) {
@@ -217,15 +231,27 @@ function copyAndroidMetadata(to) {
217
231
  const nativeDir = join2(androidDir, "fastlane/metadata/android");
218
232
  const resourcesDir = join2(OptionHolder.resourcesDir, "metadata/android");
219
233
  if (to === "resouces") {
220
- copy(nativeDir, resourcesDir);
221
- logger.done(
222
- `Android Metadata saved in ${chalk3.inverse(relativePath(OptionHolder.projectDir, resourcesDir))}. You can edit inside contents.`
223
- );
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
+ }
224
244
  } else {
225
- copy(resourcesDir, nativeDir);
226
- logger.done(
227
- `Android Metadata copyed to ${chalk3.inverse(relativePath(OptionHolder.projectDir, nativeDir))}`
228
- );
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
+ }
229
255
  }
230
256
  }
231
257
 
@@ -250,7 +276,7 @@ async function prepareAndroid() {
250
276
  "Hydrate template placeholders",
251
277
  injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
252
278
  );
253
- await exe({ cwd: destDir })`bundle install`;
279
+ await exe("bundle", ["install"], { cwd: destDir });
254
280
  logger.success("Bunder Install - Done");
255
281
  copyAndroidMetadata("native");
256
282
  }
@@ -267,7 +293,7 @@ async function replaceAndroidSigningConfig(destDir) {
267
293
  `
268
294
  signingConfigs {
269
295
  debug {
270
- storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.resourcesDir, "key", "android_release_keystore.jks"))}')
296
+ storeFile file('${relativePath(buildGradleDirPath, resolve2(OptionHolder.resourcesDir, "key", "android_release.keystore"))}')
271
297
  storePassword '${android_keystore_store_password}'
272
298
  keyAlias '${android_keystore_key_alias}'
273
299
  keyPassword '${android_keystore_key_password}'
@@ -297,6 +323,8 @@ function throwError(message, e) {
297
323
 
298
324
  // src/util/setup/VersionUtil.ts
299
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*?;?/;
300
328
  async function parseBinaryVersions() {
301
329
  const files = readdir(OptionHolder.projectDir);
302
330
  const jsConfigFiles = ["app.config.ts", "app.config.js", "app.config.cjs", "app.config.mjs"];
@@ -315,8 +343,8 @@ async function parseBinaryVersions() {
315
343
  async function parseFromJsConfigFile(filename) {
316
344
  const filePath = resolve2(OptionHolder.projectDir, filename);
317
345
  const content = read(filePath);
318
- const VERSION_NAME = /const VERSION_NAME = '(.*?)';/.exec(content)?.[1];
319
- 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];
320
348
  if (!VERSION_NAME) {
321
349
  throwError("cannot parse version name");
322
350
  }
@@ -390,7 +418,7 @@ async function prepareIos() {
390
418
  "Hydrate template placeholders",
391
419
  injectTemplatePlaceHolders(resolve2(destDir, "fastlane"))
392
420
  );
393
- await exe({ cwd: destDir })`bundle install`;
421
+ await exe("bundle", ["install"], { cwd: destDir });
394
422
  logger.success("Bunder Install - Done");
395
423
  copyIosMetadata("native");
396
424
  }
@@ -426,6 +454,21 @@ var InqueryInputs = {
426
454
  });
427
455
  }
428
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
+ },
429
472
  podinstall: async () => {
430
473
  if (OptionHolder.input.platform === "ios") {
431
474
  OptionHolder.input.pod = await select({
@@ -503,7 +546,7 @@ function getIosFastlaneOptions() {
503
546
  ...Object.entries(OptionHolder.keyholderMap).map(([k, v]) => `${k}:${v}`),
504
547
  `version_name:${OptionHolder.versionName}`,
505
548
  `version_code:${OptionHolder.versionCode}`,
506
- `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")}`,
507
550
  `upload_metadata:${OptionHolder.input.uploadMetadata}`,
508
551
  `ipa_path:${resolve2(OptionHolder.resourcesDir, "output", "ios", "app.ipa")}`,
509
552
  `ipa_dir:${resolve2(OptionHolder.resourcesDir, "output", "ios")}`,
@@ -518,7 +561,6 @@ async function build({ options }) {
518
561
  await setup();
519
562
  await promptInputs();
520
563
  logger.info("Build Started");
521
- logger.info(`Version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
522
564
  if (OptionHolder.input.platform === "ios") {
523
565
  await buildIos();
524
566
  logger.success(`iOS Build ${chalk5.bold.inverse(calculateElapsed(startTime))}`);