expo-release-it 0.0.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/build/command/bump.js +36 -0
  4. package/build/command/init.js +99 -0
  5. package/build/command/release.js +86 -0
  6. package/build/command/submit.js +65 -0
  7. package/build/index.js +46 -0
  8. package/build/util/EnvUtil.js +1 -0
  9. package/build/util/EnvironmentPreparationUtil.js +41 -0
  10. package/build/util/FileUtil.js +56 -0
  11. package/build/util/Log.js +9 -0
  12. package/build/util/Logger.js +9 -0
  13. package/build/util/OptionHolder.js +21 -0
  14. package/build/util/VersionUtil.js +89 -0
  15. package/build/util/calculateElapsed.js +7 -0
  16. package/build/util/hydrateKeyholderFileValues.js +5 -0
  17. package/build/util/iterateAllFilesInGeneratedTemplate.js +18 -0
  18. package/build/util/logutil.js +9 -0
  19. package/build/util/parseBinaryVersion.js +59 -0
  20. package/build/util/pathfinder.js +5 -0
  21. package/build/util/prepareAndroid.js +34 -0
  22. package/build/util/prepareIos.js +17 -0
  23. package/build/util/promptCommonInputs.js +27 -0
  24. package/build/util/setup.js +8 -0
  25. package/build/util/throwError.js +10 -0
  26. package/package.json +46 -0
  27. package/template/fastlane-android/Gemfile +3 -0
  28. package/template/fastlane-android/fastlane/Appfile +2 -0
  29. package/template/fastlane-android/fastlane/Fastfile +40 -0
  30. package/template/fastlane-android/fastlane/metadata/android/ko-KR/changelogs/default.txt +1 -0
  31. package/template/fastlane-android/fastlane/metadata/android/ko-KR/full_description.txt +0 -0
  32. package/template/fastlane-android/fastlane/metadata/android/ko-KR/short_description.txt +0 -0
  33. package/template/fastlane-android/fastlane/metadata/android/ko-KR/title.txt +0 -0
  34. package/template/fastlane-android/fastlane/metadata/android/ko-KR/video.txt +0 -0
  35. package/template/fastlane-ios/Gemfile +4 -0
  36. package/template/fastlane-ios/fastlane/Appfile +4 -0
  37. package/template/fastlane-ios/fastlane/Fastfile +78 -0
  38. package/template/fastlane-ios/fastlane/Matchfile +5 -0
  39. package/template/fastlane-ios/fastlane/metadata/copyright.txt +0 -0
  40. package/template/fastlane-ios/fastlane/metadata/ko/apple_tv_privacy_policy.txt +0 -0
  41. package/template/fastlane-ios/fastlane/metadata/ko/description.txt +0 -0
  42. package/template/fastlane-ios/fastlane/metadata/ko/keywords.txt +0 -0
  43. package/template/fastlane-ios/fastlane/metadata/ko/marketing_url.txt +0 -0
  44. package/template/fastlane-ios/fastlane/metadata/ko/name.txt +0 -0
  45. package/template/fastlane-ios/fastlane/metadata/ko/privacy_url.txt +0 -0
  46. package/template/fastlane-ios/fastlane/metadata/ko/promotional_text.txt +0 -0
  47. package/template/fastlane-ios/fastlane/metadata/ko/release_notes.txt +0 -0
  48. package/template/fastlane-ios/fastlane/metadata/ko/subtitle.txt +0 -0
  49. package/template/fastlane-ios/fastlane/metadata/ko/support_url.txt +0 -0
  50. package/template/fastlane-ios/fastlane/metadata/primary_category.txt +0 -0
  51. package/template/fastlane-ios/fastlane/metadata/primary_first_sub_category.txt +1 -0
  52. package/template/fastlane-ios/fastlane/metadata/primary_second_sub_category.txt +1 -0
  53. package/template/fastlane-ios/fastlane/metadata/review_information/demo_password.txt +0 -0
  54. package/template/fastlane-ios/fastlane/metadata/review_information/demo_user.txt +0 -0
  55. package/template/fastlane-ios/fastlane/metadata/review_information/email_address.txt +0 -0
  56. package/template/fastlane-ios/fastlane/metadata/review_information/first_name.txt +0 -0
  57. package/template/fastlane-ios/fastlane/metadata/review_information/last_name.txt +0 -0
  58. package/template/fastlane-ios/fastlane/metadata/review_information/notes.txt +1 -0
  59. package/template/fastlane-ios/fastlane/metadata/review_information/phone_number.txt +0 -0
  60. package/template/fastlane-ios/fastlane/metadata/secondary_category.txt +1 -0
  61. package/template/fastlane-ios/fastlane/metadata/secondary_first_sub_category.txt +1 -0
  62. package/template/fastlane-ios/fastlane/metadata/secondary_second_sub_category.txt +1 -0
  63. package/template/fastlane-ios/fastlane/screenshots/README.txt +30 -0
  64. package/template/key/android_play_console_service_account.json +1 -0
  65. package/template/key/android_release_keystore.jks +0 -0
  66. package/template/key/android_upload_certificate.pem +2 -0
  67. package/template/key/ios_provision_profile.mobileprovision +0 -0
  68. package/template/key/ios_testflight_upload_api_key.p8 +3 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 MJ Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # expo-local-build
2
+
3
+ Construct environment for Build & Submit Expo CNG Project locally.
4
+
5
+ # Placeholers
6
+
7
+
@@ -0,0 +1,36 @@
1
+ import { injectBinaryVersions } from '../util/VersionUtil';
2
+ import { throwError } from '../util/throwError';
3
+ import { logger } from '../util/logger';
4
+ import { OptionHolder } from '../util/OptionHolder';
5
+ import semver from 'semver';
6
+ import { isDev } from '../util/EnvUtil';
7
+ import { $ } from 'zx';
8
+ import { setup } from '../util/setup';
9
+ export async function bump({}) {
10
+ await preCheck();
11
+ await setup();
12
+ logger.info(`current version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
13
+ const nextVersionName = semver.inc(OptionHolder.versionName, 'patch');
14
+ const nextVersionCode = Number(OptionHolder.versionCode) + 1 + '';
15
+ logger.info(`next version: ${nextVersionName}(${nextVersionCode})`);
16
+ await injectBinaryVersions({ versionName: nextVersionName, versionCode: nextVersionCode });
17
+ }
18
+ async function preCheck() {
19
+ if (isDev)
20
+ return;
21
+ try {
22
+ let hasChange = false;
23
+ try {
24
+ await $ `git diff --quiet HEAD`;
25
+ }
26
+ catch (_) {
27
+ hasChange = true;
28
+ }
29
+ if (hasChange) {
30
+ throwError('Workspace is not clean. Before bump version, you should commit all changes.');
31
+ }
32
+ }
33
+ catch (e) {
34
+ throwError('Precheck Failed', e);
35
+ }
36
+ }
@@ -0,0 +1,99 @@
1
+ import { OptionHolder } from '../util/OptionHolder';
2
+ import { logger } from '../util/logger';
3
+ import chalk from 'chalk';
4
+ import { input } from '@inquirer/prompts';
5
+ import { iterateAllFilesInGeneratedTemplate } from '../util/iterateAllFilesInGeneratedTemplate';
6
+ import { isDev } from '../util/EnvUtil';
7
+ import { parseBinaryVersions } from '../util/VersionUtil';
8
+ import { promptCommonInputs } from '../util/promptCommonInputs';
9
+ import { exist, remove, copy, read, write, join, writeJson } from '../util/FileUtil';
10
+ import { path } from 'zx';
11
+ export async function init() {
12
+ logger.info(`${chalk.inverse('expo-local-build')}`);
13
+ await promptCommonInputs();
14
+ await parseBinaryVersions();
15
+ await promptInputs();
16
+ await copyTemplates();
17
+ await injectPlaceHolders();
18
+ logger.success('expo-local-build init success');
19
+ }
20
+ async function promptInputs() {
21
+ /* ios */
22
+ OptionHolder.iosBundleIdentifier = OptionHolder.templateValuePlaceholderMap.ios_app_identifier =
23
+ await input({
24
+ message: 'IOS Bundle Identifier',
25
+ required: true,
26
+ default: isDev ? 'ios_app_identifier' : undefined,
27
+ });
28
+ OptionHolder.templateValuePlaceholderMap.ios_app_store_connect_team_id = await input({
29
+ message: 'IOS App Store Connect Team Id',
30
+ default: isDev ? 'ios_app_store_connect_team_id' : undefined,
31
+ });
32
+ OptionHolder.templateValuePlaceholderMap.ios_developer_team_id = await input({
33
+ message: 'IOS Developer Portal Team Id',
34
+ default: isDev ? 'ios_developer_team_id' : undefined,
35
+ });
36
+ OptionHolder.templateValuePlaceholderMap.ios_match_git_url = await input({
37
+ message: 'IOS Match Key Storage Github URL',
38
+ default: isDev ? 'ios_match_git_url' : undefined,
39
+ });
40
+ OptionHolder.keyholderFileValueMap.ios_match_password = await input({
41
+ message: 'IOS Match Password',
42
+ default: isDev ? 'ios_match_password' : undefined,
43
+ });
44
+ OptionHolder.templateValuePlaceholderMap.ios_app_store_connect_api_key_id = await input({
45
+ message: 'IOS App Store Connect Api Key Id',
46
+ default: isDev ? 'ios_app_store_connect_api_key_id' : undefined,
47
+ });
48
+ OptionHolder.templateValuePlaceholderMap.ios_app_store_connect_api_key_issuer_id = await input({
49
+ message: 'IOS App Store Connect Api Key Issuer Id',
50
+ default: isDev ? 'ios_app_store_connect_api_key_issuer_id' : undefined,
51
+ });
52
+ OptionHolder.templateValuePlaceholderMap.ios_xcode_project_target = await input({
53
+ message: 'IOS Xcode Target Name',
54
+ default: 'app',
55
+ });
56
+ /* android */
57
+ OptionHolder.androidPackageName = OptionHolder.templateValuePlaceholderMap.android_package_name =
58
+ await input({
59
+ message: 'Android Package Name',
60
+ default: isDev ? 'android_package_name' : undefined,
61
+ });
62
+ OptionHolder.keyholderFileValueMap.android_keystore_store_password = await input({
63
+ message: 'Android Keystore Store Password',
64
+ default: isDev ? 'android_keystore_store_password' : undefined,
65
+ });
66
+ OptionHolder.keyholderFileValueMap.android_keystore_key_alias = await input({
67
+ message: 'Android Keystore Key Alias',
68
+ default: isDev ? 'android_keystore_key_alias' : undefined,
69
+ });
70
+ OptionHolder.keyholderFileValueMap.android_keystore_key_password = await input({
71
+ message: 'Android Keystore Key Password',
72
+ default: isDev ? 'android_keystore_key_password' : undefined,
73
+ });
74
+ writeJson(OptionHolder.keyholderFilePath, OptionHolder.keyholderFileValueMap);
75
+ }
76
+ async function copyTemplates() {
77
+ for (const dir of OptionHolder.templateDirNames) {
78
+ await copyDirRecursively(dir);
79
+ }
80
+ async function copyDirRecursively(dir) {
81
+ const sourceDirPath = join(OptionHolder.cli.templateDir, dir);
82
+ const destDirPath = join(OptionHolder.outDir, dir);
83
+ if (exist(destDirPath)) {
84
+ remove(destDirPath);
85
+ }
86
+ copy(sourceDirPath, destDirPath);
87
+ }
88
+ }
89
+ async function injectPlaceHolders() {
90
+ await iterateAllFilesInGeneratedTemplate(async (filePath) => {
91
+ let content = read(filePath);
92
+ for (const [key, value] of Object.entries(OptionHolder.templateValuePlaceholderMap)) {
93
+ content = content.replaceAll(`{{${key}}}`, value);
94
+ }
95
+ // dynamic replace
96
+ content = content.replaceAll('{{key_dir_relative_path}}', path.relative(filePath, OptionHolder.keyDir));
97
+ write(filePath, content);
98
+ });
99
+ }
@@ -0,0 +1,86 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { prepareAndroid } from '../util/prepareAndroid';
3
+ import { setup } from '../util/setup';
4
+ import { prepareIos } from '../util/prepareIos';
5
+ import { logger } from '../util/logger';
6
+ import { OptionHolder } from '../util/OptionHolder';
7
+ import { spinner, $ } from 'zx';
8
+ import { resolve, remove } from '../util/FileUtil';
9
+ import chalk from 'chalk';
10
+ import { calculateElapsed } from '../util/calculateElapsed';
11
+ const releaseOptions = {
12
+ platform: 'ios',
13
+ pod: true,
14
+ };
15
+ export async function release() {
16
+ const startTime = Date.now();
17
+ await setup();
18
+ await promptInputs();
19
+ logger.info('Build & Upload Started');
20
+ logger.info(`Version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
21
+ if (releaseOptions.platform === 'ios') {
22
+ await releaseIos();
23
+ logger.success(`iOS Release Success ${chalk.bold.inverse(calculateElapsed(startTime))}`);
24
+ }
25
+ else {
26
+ await releaseAndroid();
27
+ logger.success(`Android Release Success ${chalk.bold.inverse(calculateElapsed(startTime))}`);
28
+ }
29
+ }
30
+ async function promptInputs() {
31
+ releaseOptions.platform = await select({
32
+ message: 'Platform to release',
33
+ choices: [
34
+ { name: 'ios', value: 'ios', description: 'Release ios' },
35
+ { name: 'android', value: 'android', description: 'Release android' },
36
+ ],
37
+ });
38
+ if (releaseOptions.platform === 'ios') {
39
+ releaseOptions.pod = await select({
40
+ message: 'Install Cocoapods',
41
+ choices: [
42
+ { name: 'yes', value: true, description: 'Install pods before release' },
43
+ { name: 'no', value: false, description: 'Skip pods install' },
44
+ ],
45
+ });
46
+ }
47
+ }
48
+ async function releaseIos() {
49
+ const iosDir = resolve(OptionHolder.rootDir, 'ios');
50
+ const $$ = $({
51
+ verbose: false,
52
+ cwd: iosDir,
53
+ env: {
54
+ MATCH_PASSWORD: OptionHolder.keyholderFileValueMap.ios_match_password,
55
+ },
56
+ });
57
+ await prepareIos();
58
+ await fastlane();
59
+ async function fastlane() {
60
+ await spinner('Bundler Install', () => $$ `bundle install`);
61
+ logger.success('Bundler Install');
62
+ if (releaseOptions.pod) {
63
+ await spinner('Pod Install', () => $$ `bundle exec pod install`);
64
+ logger.success('Pod Install');
65
+ }
66
+ remove(resolve(iosDir, '.xcode.env.local'));
67
+ await spinner('Fastlane', () => $$ `bundle exec fastlane release version_name:${OptionHolder.versionName} version_code:${OptionHolder.versionCode}`);
68
+ logger.success('Deploy to App Store Connect Test Flight');
69
+ }
70
+ }
71
+ async function releaseAndroid() {
72
+ const $$ = $({
73
+ verbose: false,
74
+ cwd: resolve(OptionHolder.rootDir, 'android'),
75
+ });
76
+ await prepareAndroid();
77
+ await fastlane();
78
+ async function fastlane() {
79
+ await spinner('Bundler Install', () => $$ `bundle install`);
80
+ logger.success('Bundler Install');
81
+ await spinner('Bundle AAB', () => $$ `./gradlew app:bundleRelease`);
82
+ logger.success('Bundle AAB');
83
+ await spinner('Fastlane Supply', () => $$ `bundle exec fastlane deploy version_name:${OptionHolder.versionName} version_code:${OptionHolder.versionCode}`);
84
+ logger.success('Deploy to Play Store Internal Testing Track');
85
+ }
86
+ }
@@ -0,0 +1,65 @@
1
+ import { setup } from '../util/setup';
2
+ import { select } from '@inquirer/prompts';
3
+ import { logger } from '../util/logger';
4
+ import { OptionHolder } from '../util/OptionHolder';
5
+ import chalk from 'chalk';
6
+ import { calculateElapsed } from '../util/calculateElapsed';
7
+ import { spinner, $ } from 'zx';
8
+ import { prepareAndroid } from '../util/prepareAndroid';
9
+ import { remove, resolve } from '../util/FileUtil';
10
+ import { prepareIos } from '../util/prepareIos';
11
+ const releaseOptions = {
12
+ platform: 'ios',
13
+ };
14
+ export async function submit() {
15
+ const startTime = Date.now();
16
+ await setup();
17
+ await promptInputs();
18
+ logger.info('Submit Started');
19
+ logger.info(`Version: ${OptionHolder.versionName}(${OptionHolder.versionCode})`);
20
+ if (releaseOptions.platform === 'ios') {
21
+ await submitIos();
22
+ logger.success(`Appstore Review Submitted ${chalk.bold.inverse(calculateElapsed(startTime))}`);
23
+ }
24
+ else {
25
+ await submitAndroid();
26
+ logger.success(`Playstore Review Submitted ${chalk.bold.inverse(calculateElapsed(startTime))}`);
27
+ }
28
+ }
29
+ async function promptInputs() {
30
+ releaseOptions.platform = await select({
31
+ message: 'Platform to release',
32
+ choices: [
33
+ { name: 'ios', value: 'ios', description: 'Release ios' },
34
+ { name: 'android', value: 'android', description: 'Release android' },
35
+ ],
36
+ });
37
+ }
38
+ async function submitAndroid() {
39
+ const $$ = $({
40
+ verbose: false,
41
+ cwd: resolve(OptionHolder.rootDir, 'android'),
42
+ });
43
+ await prepareAndroid();
44
+ await fastlane();
45
+ async function fastlane() {
46
+ await spinner('Bundler Install', () => $$ `bundle install`);
47
+ logger.success('Bundler Install');
48
+ await spinner('Fastlane', () => $$ `bundle exec fastlane submit_playstore_review version_name:${OptionHolder.versionName} version_code:${OptionHolder.versionCode}`);
49
+ }
50
+ }
51
+ async function submitIos() {
52
+ const iosDir = resolve(OptionHolder.rootDir, 'ios');
53
+ const $$ = $({
54
+ verbose: false,
55
+ cwd: iosDir,
56
+ });
57
+ await prepareIos();
58
+ await fastlane();
59
+ async function fastlane() {
60
+ await spinner('Bundler Install', () => $$ `bundle install`);
61
+ logger.success('Bundler Install');
62
+ remove(resolve(iosDir, '.xcode.env.local'));
63
+ await spinner('Fastlane', () => $$ `bundle exec fastlane submit_appstore_review version_name:${OptionHolder.versionName} version_code:${OptionHolder.versionCode}`);
64
+ }
65
+ }
package/build/index.js ADDED
@@ -0,0 +1,46 @@
1
+ import { Command } from 'commander';
2
+ import { init } from './command/init';
3
+ import { OptionHolder } from './util/OptionHolder';
4
+ import { logger } from './util/logger';
5
+ import { bump } from './command/bump';
6
+ import { release } from './command/release';
7
+ import { submit } from './command/submit';
8
+ const program = new Command();
9
+ program
10
+ .name('expo-local-cicd')
11
+ .description('generate CLI script to Build & Submit local Expo project')
12
+ .version(OptionHolder.cli.version);
13
+ program
14
+ .command('init')
15
+ .description('Initialize CLI utilities')
16
+ .action(async () => {
17
+ try {
18
+ await init();
19
+ }
20
+ catch (e) {
21
+ logger.error(e);
22
+ }
23
+ });
24
+ program
25
+ .command('bump')
26
+ .description('Bump binary release patch version with modifying app.json')
27
+ // .addOption(
28
+ // new Option('-i --increment <type>', 'increment type').choices(['major', 'minor', 'patch']),
29
+ // )
30
+ .action(async (options) => {
31
+ await bump({ options });
32
+ });
33
+ program
34
+ .command('release')
35
+ .description('Build binary & upload to internal test track(android) and testflight(ios)')
36
+ .option('-p --platform <platform>', 'Platform', 'ios')
37
+ .action(async () => {
38
+ await release();
39
+ });
40
+ program
41
+ .command('submit')
42
+ .description('Submit Google Play Console & App Store Connect Review with latest testing track')
43
+ .action(async () => {
44
+ await submit();
45
+ });
46
+ program.parse();
@@ -0,0 +1 @@
1
+ export const isDev = process.env.NODE_ENV === 'development';
@@ -0,0 +1,41 @@
1
+ import { spinner, $ } from 'zx';
2
+ import { OptionHolder } from './OptionHolder';
3
+ import { iterateDir, remove, resolve, copy, read, write } from './FileUtil';
4
+ import { logger } from './logger';
5
+ export async function prepareAndroid() {
6
+ await spinner('Prebuild Android', () => $ `expo prebuild -p android --no-install`);
7
+ const androidDir = resolve(OptionHolder.rootDir, 'android');
8
+ const fastlaneSrcDir = resolve(OptionHolder.rootDir, 'fastlane-android');
9
+ const fastlaneDestDir = resolve(OptionHolder.rootDir, 'android', 'fastlane');
10
+ remove(fastlaneDestDir);
11
+ await iterateDir(fastlaneSrcDir, async (file) => {
12
+ const filePath = resolve(fastlaneSrcDir, file);
13
+ copy(filePath, resolve(androidDir, file));
14
+ });
15
+ logger.info('Inject Android Fastlane files');
16
+ await replaceAndroidSigningConfig();
17
+ async function replaceAndroidSigningConfig() {
18
+ const buildGradlePath = resolve('android', 'app', 'build.gradle');
19
+ const content = read(buildGradlePath).replace(/signingConfigs.*?{.*?debug.*?{.*?}.*?}/s, `
20
+ signingConfigs {
21
+ debug {
22
+ storeFile file('${resolve(OptionHolder.outDir, 'key', 'android_release_keystore.jks')}')
23
+ storePassword '{{android_keystore_store_password}}'
24
+ keyAlias '{{android_keystore_key_alias}}'
25
+ keyPassword '{{android_keystore_key_password}}'
26
+ }
27
+ }
28
+ `);
29
+ write(buildGradlePath, content);
30
+ logger.info('Replace Android Signing Config');
31
+ }
32
+ }
33
+ // export async function prepareIos() {
34
+ // await spinner('Prebuild iOS', () => $`expo prebuild -p ios --no-install`);
35
+ // await remove('ios/fastlane');
36
+ // await iterateDir('fastlane-ios', async (file) => {
37
+ // const filePath = resolve('fastlane-ios', file);
38
+ // await $`rm -rf ios/${file} && cp -r ${filePath} ios/${file}`;
39
+ // });
40
+ // printSuccess('Inject iOS Fastlane files');
41
+ // }
@@ -0,0 +1,56 @@
1
+ import fs from 'fs-extra';
2
+ import * as path from 'path';
3
+ export const join = path.join;
4
+ export const resolve = path.resolve;
5
+ export const filename = path.basename(__filename);
6
+ export function exist(path) {
7
+ return fs.existsSync(path);
8
+ }
9
+ export function isDir(path) {
10
+ return exist(path) && fs.lstatSync(path).isDirectory();
11
+ }
12
+ export function isFile(path) {
13
+ return exist(path) && fs.lstatSync(path).isFile();
14
+ }
15
+ export function read(path) {
16
+ return fs.readFileSync(path, { encoding: 'utf8' });
17
+ }
18
+ export function readdir(path) {
19
+ return fs.readdirSync(path);
20
+ }
21
+ // you should require when possible(optimized in js)
22
+ export function readJsonSlow(path) {
23
+ return fs.readJSONSync(path);
24
+ }
25
+ export function write(p, content) {
26
+ const dir = path.dirname(p);
27
+ if (!exist(dir)) {
28
+ fs.mkdirSync(dir, { recursive: true });
29
+ }
30
+ return fs.writeFileSync(p, content);
31
+ }
32
+ export function copy(src, dest) {
33
+ fs.copySync(src, dest);
34
+ }
35
+ export function writeJson(path, json) {
36
+ return write(path, JSON.stringify(json, null, 2));
37
+ }
38
+ export function remove(path) {
39
+ if (!exist(path)) {
40
+ return;
41
+ }
42
+ if (fs.lstatSync(path).isDirectory()) {
43
+ return fs.rmSync(path, { force: true, recursive: true });
44
+ }
45
+ else {
46
+ return fs.rmSync(path, { force: true });
47
+ }
48
+ }
49
+ export async function iterateDir(path, fn) {
50
+ if (!isDir(path)) {
51
+ return;
52
+ }
53
+ for (const file of fs.readdirSync(path)) {
54
+ await fn(file);
55
+ }
56
+ }
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk';
2
+ export const log = {
3
+ success: (...msg) => console.log(chalk.green(`✔ ${msg.join(', ')}`)),
4
+ error: (...msg) => console.log(chalk.red(`✖ ${msg.join(', ')}`)),
5
+ warn: (...msg) => console.log(chalk.yellow(`⚠ ${msg.join(', ')}`)),
6
+ info: (...msg) => console.log(chalk.cyan(`ℹ ${msg.join(', ')}`)),
7
+ title: (...msg) => console.log(chalk.bold.blue(`\n${msg.join(', ')}`)),
8
+ dim: (...msg) => console.log(chalk.dim(msg.join(', '))),
9
+ };
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk';
2
+ export const logger = {
3
+ success: (...msg) => console.log(chalk.green(`✔ ${msg.join(', ')}`)),
4
+ error: (...msg) => console.log(chalk.red(`✖ ${msg.join(', ')}`)),
5
+ warn: (...msg) => console.log(chalk.yellow(`⚠ ${msg.join(', ')}`)),
6
+ info: (...msg) => console.log(chalk.cyan(`ℹ ${msg.join(', ')}`)),
7
+ title: (...msg) => console.log(chalk.bold.blue(`\n${msg.join(', ')}`)),
8
+ dim: (...msg) => console.log(chalk.dim(msg.join(', '))),
9
+ };
@@ -0,0 +1,21 @@
1
+ import pkgJson from '../../package.json';
2
+ import { fileURLToPath } from 'url';
3
+ import { resolve } from './FileUtil';
4
+ export const OptionHolder = {
5
+ cli: {
6
+ version: pkgJson.version,
7
+ rootDir: resolve(fileURLToPath(import.meta.url), '../../..'),
8
+ templateDir: resolve(fileURLToPath(import.meta.url), '../../../template'),
9
+ },
10
+ templateValuePlaceholderMap: {},
11
+ keyholderFileValueMap: {},
12
+ templateDirNames: ['fastlane-android', 'fastlane-ios', 'key'],
13
+ iosBundleIdentifier: '',
14
+ androidPackageName: '',
15
+ rootDir: '',
16
+ outDir: '',
17
+ keyDir: '',
18
+ keyholderFilePath: '',
19
+ versionName: '',
20
+ versionCode: '',
21
+ };
@@ -0,0 +1,89 @@
1
+ import { OptionHolder } from './OptionHolder';
2
+ import { throwError } from './throwError';
3
+ import semver from 'semver';
4
+ import { readdir, read, readJsonSlow, write, writeJson, resolve } from './FileUtil';
5
+ export async function parseBinaryVersions() {
6
+ const files = readdir(OptionHolder.rootDir);
7
+ const jsConfigFiles = ['app.config.ts', 'app.config.js', 'app.config.cjs', 'app.config.mjs'];
8
+ const jsonConfigFile = 'app.json';
9
+ for (const jsConfigFile of jsConfigFiles) {
10
+ if (files.includes(jsConfigFile)) {
11
+ await parseFromJsConfigFile(jsConfigFile);
12
+ return;
13
+ }
14
+ }
15
+ if (files.includes(jsonConfigFile)) {
16
+ await parseFromJsonConfigFile(jsonConfigFile);
17
+ return;
18
+ }
19
+ throwError('expo config file not found');
20
+ async function parseFromJsConfigFile(filename) {
21
+ const filePath = resolve(OptionHolder.rootDir, filename);
22
+ const content = read(filePath);
23
+ const VERSION_NAME = /const VERSION_NAME = '(.*?)';/.exec(content)?.[1];
24
+ const VERSION_CODE = /const VERSION_CODE = (.*?);/.exec(content)?.[1];
25
+ if (!VERSION_NAME) {
26
+ throwError('cannot parse version name');
27
+ }
28
+ if (!VERSION_CODE) {
29
+ throwError('cannot parse version code');
30
+ }
31
+ if (!semver.valid(VERSION_NAME)) {
32
+ throwError('VERSION_NAME is not a valid semver format');
33
+ }
34
+ OptionHolder.versionName = VERSION_NAME;
35
+ OptionHolder.versionCode = VERSION_CODE;
36
+ }
37
+ async function parseFromJsonConfigFile(filename) {
38
+ const filePath = resolve(OptionHolder.rootDir, filename);
39
+ const json = readJsonSlow(filePath);
40
+ const versionName = json.expo?.version + '';
41
+ const iosVersionCode = json.expo?.ios?.buildNumber + '';
42
+ const androidVersionCode = json.expo?.android?.versionCode + '';
43
+ if (!/\d+/.test(iosVersionCode)) {
44
+ throwError('app.json expo.ios.buildNumber is not an integer');
45
+ }
46
+ if (!/\d+/.test(androidVersionCode)) {
47
+ throwError('app.json expo.android.versionCode is not an integer');
48
+ }
49
+ if (iosVersionCode !== androidVersionCode) {
50
+ throwError('app.json expo.ios.buildNumber is not equal with expo.android.versionCode');
51
+ }
52
+ if (!semver.valid(versionName)) {
53
+ throwError('VERSION_NAME is not a valid semver format');
54
+ }
55
+ OptionHolder.versionName = versionName;
56
+ OptionHolder.versionCode = iosVersionCode;
57
+ }
58
+ }
59
+ export async function injectBinaryVersions({ versionCode, versionName, }) {
60
+ const files = readdir(OptionHolder.rootDir);
61
+ const jsConfigFiles = ['app.config.ts', 'app.config.js', 'app.config.cjs', 'app.config.mjs'];
62
+ const jsonConfigFile = 'app.json';
63
+ for (const jsConfigFile of jsConfigFiles) {
64
+ if (files.includes(jsConfigFile)) {
65
+ await injectToJsConfigFile(jsConfigFile);
66
+ return;
67
+ }
68
+ }
69
+ if (files.includes(jsonConfigFile)) {
70
+ await injectToJsonConfigFile(jsonConfigFile);
71
+ return;
72
+ }
73
+ throwError('expo config file not found');
74
+ async function injectToJsConfigFile(filename) {
75
+ const filePath = resolve(OptionHolder.rootDir, filename);
76
+ let content = read(filePath);
77
+ content = content.replace(/const VERSION_NAME = '(.*?)';/, `const VERSION_NAME = '${versionName}';`);
78
+ content = content.replace(/const VERSION_CODE = (.*?);/, `const VERSION_CODE = ${versionCode};`);
79
+ write(filePath, content);
80
+ }
81
+ async function injectToJsonConfigFile(filename) {
82
+ const filePath = resolve(OptionHolder.rootDir, filename);
83
+ const json = readJsonSlow(filePath);
84
+ json.expo.version = versionName;
85
+ json.expo.ios.buildNumber = versionCode;
86
+ json.expo.android.versionCode = +versionCode;
87
+ writeJson(filePath, json);
88
+ }
89
+ }
@@ -0,0 +1,7 @@
1
+ import { padZero } from '@mj-studio/js-util';
2
+ export function calculateElapsed(t) {
3
+ let seconds = Math.floor((Date.now() - t) / 1000);
4
+ const minutes = Math.floor(seconds / 60);
5
+ seconds = seconds % 60;
6
+ return `${minutes}:${padZero(seconds)}`;
7
+ }
@@ -0,0 +1,5 @@
1
+ import { OptionHolder } from './OptionHolder';
2
+ import { readJsonSlow } from './FileUtil';
3
+ export async function hydrateKeyholderFileValues() {
4
+ Object.assign(OptionHolder.keyholderFileValueMap, readJsonSlow(OptionHolder.keyholderFilePath));
5
+ }
@@ -0,0 +1,18 @@
1
+ import { OptionHolder } from './OptionHolder';
2
+ import { readdir, isFile, isDir, join } from './FileUtil';
3
+ export async function iterateAllFilesInGeneratedTemplate(callback) {
4
+ for (const dir of OptionHolder.templateDirNames) {
5
+ await go(join(OptionHolder.outDir, dir));
6
+ }
7
+ async function go(dirPath) {
8
+ const paths = readdir(dirPath).map((f) => join(dirPath, f));
9
+ for (const p of paths) {
10
+ if (isFile(p)) {
11
+ await callback(p);
12
+ }
13
+ else if (isDir(p)) {
14
+ await go(p);
15
+ }
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk';
2
+ export const log = {
3
+ success: (msg) => console.log(chalk.green(`✔ ${msg}`)),
4
+ error: (msg) => console.log(chalk.red(`✖ ${msg}`)),
5
+ warn: (msg) => console.log(chalk.yellow(`⚠ ${msg}`)),
6
+ info: (msg) => console.log(chalk.cyan(`ℹ ${msg}`)),
7
+ title: (msg) => console.log(chalk.bold.blue(`\n${msg}`)),
8
+ dim: (msg) => console.log(chalk.dim(msg)),
9
+ };
@@ -0,0 +1,59 @@
1
+ import fs from 'fs-extra';
2
+ import { OptionHolder } from './OptionHolder';
3
+ import * as path from 'path';
4
+ import { throwError } from './throwError';
5
+ import semver from 'semver';
6
+ export async function parseBinaryVersion() {
7
+ const files = await fs.readdir(OptionHolder.rootDir);
8
+ const jsConfigFiles = ['app.config.ts', 'app.config.js', 'app.config.cjs', 'app.config.mjs'];
9
+ const jsonConfigFile = 'app.json';
10
+ for (const jsConfigFile of jsConfigFiles) {
11
+ if (files.includes(jsConfigFile)) {
12
+ await parseFromJsConfigFile(jsConfigFile);
13
+ return;
14
+ }
15
+ }
16
+ if (files.includes(jsonConfigFile)) {
17
+ await parseFromJsonConfigFile(jsonConfigFile);
18
+ return;
19
+ }
20
+ throwError('expo config file not found');
21
+ async function parseFromJsConfigFile(filename) {
22
+ const filePath = path.resolve(OptionHolder.rootDir, filename);
23
+ const content = await fs.readFile(filePath, { encoding: 'utf-8' });
24
+ const VERSION_NAME = /const VERSION_NAME = '(.*?)';?/.exec(content)?.[1];
25
+ const VERSION_CODE = /const VERSION_CODE = (.*?);?/.exec(content)?.[1];
26
+ if (!VERSION_NAME) {
27
+ throwError('cannot parse version name');
28
+ }
29
+ if (!VERSION_CODE) {
30
+ throwError('cannot parse version code');
31
+ }
32
+ if (!semver.valid(VERSION_NAME)) {
33
+ throwError('VERSION_NAME is not a valid semver format');
34
+ }
35
+ OptionHolder.versionName = VERSION_NAME;
36
+ OptionHolder.versionCode = VERSION_CODE;
37
+ }
38
+ async function parseFromJsonConfigFile(filename) {
39
+ const filePath = path.resolve(OptionHolder.rootDir, filename);
40
+ const json = JSON.parse(await fs.readFile(filePath, { encoding: 'utf-8' }));
41
+ const versionName = json.expo?.version + '';
42
+ const iosVersionCode = json.expo?.ios?.buildNumber + '';
43
+ const androidVersionCode = json.expo?.android?.versionCode + '';
44
+ if (!/\d+/.test(iosVersionCode)) {
45
+ throwError('app.json expo.ios.buildNumber is not an integer');
46
+ }
47
+ if (!/\d+/.test(androidVersionCode)) {
48
+ throwError('app.json expo.android.versionCode is not an integer');
49
+ }
50
+ if (iosVersionCode !== androidVersionCode) {
51
+ throwError('app.json expo.ios.buildNumber is not equal with expo.android.versionCode');
52
+ }
53
+ if (!semver.valid(versionName)) {
54
+ throwError('VERSION_NAME is not a valid semver format');
55
+ }
56
+ OptionHolder.versionName = versionName;
57
+ OptionHolder.versionCode = iosVersionCode;
58
+ }
59
+ }
@@ -0,0 +1,5 @@
1
+ export const PathFinder = {
2
+ rootDir: () => {
3
+ return process.cwd();
4
+ }
5
+ };
@@ -0,0 +1,34 @@
1
+ import { spinner, $, path } from 'zx';
2
+ import { resolve, remove, iterateDir, copy, read, write } from './FileUtil';
3
+ import { OptionHolder } from './OptionHolder';
4
+ import { logger } from './logger';
5
+ export async function prepareAndroid() {
6
+ const androidDir = resolve(OptionHolder.rootDir, 'android');
7
+ const fastlaneSrcDir = resolve(OptionHolder.outDir, 'fastlane-android');
8
+ const fastlaneDestDir = resolve(OptionHolder.rootDir, 'android', 'fastlane');
9
+ await spinner('Prebuild Android', () => $ `cd ${OptionHolder.rootDir} && expo prebuild -p android --no-install`);
10
+ remove(fastlaneDestDir);
11
+ await iterateDir(fastlaneSrcDir, async (file) => {
12
+ const filePath = resolve(fastlaneSrcDir, file);
13
+ remove(resolve(androidDir, file));
14
+ copy(filePath, resolve(androidDir, file));
15
+ });
16
+ logger.success('Inject Android Fastlane files');
17
+ await replaceAndroidSigningConfig();
18
+ async function replaceAndroidSigningConfig() {
19
+ const buildGradlePath = resolve(androidDir, 'app', 'build.gradle');
20
+ const { android_keystore_store_password, android_keystore_key_alias, android_keystore_key_password, } = OptionHolder.keyholderFileValueMap;
21
+ const content = read(buildGradlePath).replace(/signingConfigs.*?{.*?debug.*?{.*?}.*?}/s, `
22
+ signingConfigs {
23
+ debug {
24
+ storeFile file('${path.relative(buildGradlePath, resolve(OptionHolder.outDir, 'key', 'android_release_keystore.jks'))}')
25
+ storePassword '${android_keystore_store_password}'
26
+ keyAlias '${android_keystore_key_alias}'
27
+ keyPassword '${android_keystore_key_password}'
28
+ }
29
+ }
30
+ `);
31
+ write(buildGradlePath, content);
32
+ logger.success('Replace Android Signing Config');
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ import { $, spinner } from 'zx';
2
+ import { remove, resolve, iterateDir, copy } from './FileUtil';
3
+ import { OptionHolder } from './OptionHolder';
4
+ import { logger } from './logger';
5
+ export async function prepareIos() {
6
+ const iosDir = resolve(OptionHolder.rootDir, 'ios');
7
+ const fastlaneSrcDir = resolve(OptionHolder.outDir, 'fastlane-ios');
8
+ const fastlaneDestDir = resolve(OptionHolder.rootDir, 'ios', 'fastlane');
9
+ await spinner('Prebuild iOS', () => $ `cd ${OptionHolder.rootDir} && expo prebuild -p ios --no-install`);
10
+ remove(fastlaneDestDir);
11
+ await iterateDir(fastlaneSrcDir, async (file) => {
12
+ const filePath = resolve(fastlaneSrcDir, file);
13
+ remove(resolve(iosDir, file));
14
+ copy(filePath, resolve(iosDir, file));
15
+ });
16
+ logger.success('Inject iOS Fastlane files');
17
+ }
@@ -0,0 +1,27 @@
1
+ import { OptionHolder } from './OptionHolder';
2
+ import { input } from '@inquirer/prompts';
3
+ import { isDev } from './EnvUtil';
4
+ import { exist, resolve } from './FileUtil';
5
+ export async function promptCommonInputs() {
6
+ OptionHolder.rootDir = isDev
7
+ ? resolve('example')
8
+ : resolve(await input({
9
+ message: 'react native project root directory path',
10
+ default: isDev ? 'example' : '.',
11
+ validate: (value) => {
12
+ const p = resolve(value, 'package.json');
13
+ if (!exist(p)) {
14
+ return "package.json hasn\'t been detected. provider valid project root.";
15
+ }
16
+ return true;
17
+ },
18
+ }));
19
+ OptionHolder.outDir = isDev
20
+ ? resolve(OptionHolder.rootDir, 'expo-local-build')
21
+ : resolve(OptionHolder.rootDir, await input({
22
+ message: 'output path',
23
+ default: 'expo-local-build',
24
+ }));
25
+ OptionHolder.keyDir = resolve(OptionHolder.outDir, 'key');
26
+ OptionHolder.keyholderFilePath = resolve(OptionHolder.outDir, 'keyholder.json');
27
+ }
@@ -0,0 +1,8 @@
1
+ import { promptCommonInputs } from './promptCommonInputs';
2
+ import { hydrateKeyholderFileValues } from './hydrateKeyholderFileValues';
3
+ import { parseBinaryVersions } from './VersionUtil';
4
+ export async function setup() {
5
+ await promptCommonInputs();
6
+ await hydrateKeyholderFileValues();
7
+ await parseBinaryVersions();
8
+ }
@@ -0,0 +1,10 @@
1
+ import { logger } from './logger';
2
+ export function throwError(message, e) {
3
+ if (e) {
4
+ logger.error(message, e);
5
+ }
6
+ else {
7
+ logger.error(message);
8
+ }
9
+ process.exit(1);
10
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "expo-release-it",
3
+ "module": "src/index.ts",
4
+ "type": "module",
5
+ "version": "0.0.5",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "dev": "bun run build && NODE_ENV=development bun build/index.js",
10
+ "release": "release-it",
11
+ "android": "expo run:android",
12
+ "ios": "expo run:ios"
13
+ },
14
+ "dependencies": {
15
+ "@inquirer/prompts": "^7.4.1",
16
+ "@mj-studio/js-util": "^1.1.20",
17
+ "chalk": "^5.4.1",
18
+ "commander": "^13.1.0",
19
+ "expo": "~52.0.46",
20
+ "fs-extra": "^11.3.0",
21
+ "react": "18.3.1",
22
+ "react-native": "0.76.9",
23
+ "semver": "^7.7.1",
24
+ "url": "^0.11.4",
25
+ "zx": "^8.5.3"
26
+ },
27
+ "devDependencies": {
28
+ "@types/bun": "latest",
29
+ "@types/fs-extra": "^11.0.4",
30
+ "@types/semver": "^7.7.0",
31
+ "prettier": "3.5.3",
32
+ "release-it": "^19.0.1",
33
+ "typescript": "^5.8.3"
34
+ },
35
+ "workspaces": [
36
+ "example"
37
+ ],
38
+ "bin": "./build/index.js",
39
+ "files": [
40
+ "build",
41
+ "package.json",
42
+ "bin",
43
+ "template"
44
+ ],
45
+ "packageManager": "yarn@4.9.1"
46
+ }
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "fastlane", "2.227.0"
@@ -0,0 +1,2 @@
1
+ json_key_file("{{key_dir_relative_path}}/android_play_console_service_account.json")
2
+ package_name("{{android_package_name}}")
@@ -0,0 +1,40 @@
1
+ default_platform(:android)
2
+
3
+ platform :android do
4
+ before_all do
5
+ ENV['SUPPLY_UPLOAD_MAX_RETRIES']='3'
6
+ end
7
+ lane :deploy do |options|
8
+ supply(
9
+ root_url: "https://androidpublisher.googleapis.com/",
10
+ package_name: "{{android_package_name}}",
11
+ version_name: options[:version_name],
12
+ version_code: options[:version_code].to_i,
13
+ track: "internal",
14
+ rollout: "1.0",
15
+ aab: "app/build/outputs/bundle/release/app-release.aab",
16
+ skip_upload_metadata: true,
17
+ skip_upload_changelogs: true,
18
+ skip_upload_images: true,
19
+ skip_upload_screenshots: true
20
+ )
21
+ end
22
+ lane :submit_playstore_review do |options|
23
+ supply(
24
+ root_url: "https://androidpublisher.googleapis.com/",
25
+ package_name: "{{android_package_name}}",
26
+ skip_upload_apk: true,
27
+ skip_upload_aab: true,
28
+ version_name: options[:version_name],
29
+ version_code: options[:version_code].to_i,
30
+ track: "internal",
31
+ track_promote_to: "production",
32
+
33
+ rollout: "1.0",
34
+ skip_upload_metadata: false,
35
+ skip_upload_changelogs: false,
36
+ skip_upload_images: false,
37
+ skip_upload_screenshots: false
38
+ )
39
+ end
40
+ end
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "fastlane", "2.227.0"
4
+ gem "pry"
@@ -0,0 +1,4 @@
1
+ app_identifier("{{ios_app_identifier}}") # The bundle identifier of your app
2
+
3
+ itc_team_id("{{ios_app_store_connect_team_id}}") # App Store Connect Team ID
4
+ team_id("{{ios_developer_team_id}}") # Developer Portal Team ID
@@ -0,0 +1,78 @@
1
+ default_platform(:ios)
2
+
3
+ platform :ios do
4
+ lane :common do
5
+ api_key = app_store_connect_api_key(
6
+ key_id: "{{ios_app_store_connect_api_key_id}}",
7
+ issuer_id: "{{ios_app_store_connect_api_key_issuer_id}}",
8
+ key_filepath: "../key/ios_testflight_upload_api_key.p8",
9
+ )
10
+ Actions.lane_context[:API_KEY] = api_key
11
+ end
12
+
13
+ lane :release do |options|
14
+ common
15
+ setup_ci
16
+ match(
17
+ type: "appstore",
18
+ app_identifier: "{{ios_app_identifier}}",
19
+ verbose: false,
20
+ team_id: "{{ios_developer_team_id}}"
21
+ )
22
+
23
+ update_code_signing_settings(
24
+ use_automatic_signing: false,
25
+ path: "{{ios_xcode_project_target}}.xcodeproj",
26
+ team_id: ENV["sigh_{{ios_app_identifier}}_appstore_team-id"],
27
+ code_sign_identity: "Apple Distribution"
28
+ )
29
+
30
+ update_project_provisioning(
31
+ xcodeproj: "{{ios_xcode_project_target}}.xcodeproj",
32
+ profile: ENV["sigh_{{ios_app_identifier}}_appstore_profile-path"],
33
+ target_filter: "{{ios_xcode_project_target}}",
34
+ build_configuration: "Release",
35
+ code_signing_identity: "Apple Distribution"
36
+ )
37
+
38
+ build_app(
39
+ scheme: "{{ios_xcode_project_target}}",
40
+ silent: true,
41
+ output_directory: "output",
42
+ output_name: "output.ipa",
43
+ archive_path: "output/archive",
44
+ export_method: "app-store",
45
+ codesigning_identity: "Apple Distribution",
46
+ export_options: {
47
+ provisioningProfiles: {
48
+ "{{ios_app_identifier}}" => "match AppStore {{ios_app_identifier}}"
49
+ }
50
+ }
51
+ )
52
+
53
+ upload_to_testflight(
54
+ api_key: lane_context[:API_KEY],
55
+ app_version: options[:version_name],
56
+ app_identifier: "{{ios_app_identifier}}",
57
+ app_platform: "ios",
58
+ ipa: "output/output.ipa",
59
+ skip_submission: true,
60
+ skip_waiting_for_build_processing: true,
61
+ )
62
+ end
63
+
64
+ lane :submit_appstore_review do |options|
65
+ common
66
+ upload_to_app_store(
67
+ api_key: lane_context[:API_KEY],
68
+ app_version: options[:version_name],
69
+ force: true, # Set to true to skip verification of HTML preview
70
+ skip_binary_upload: true,
71
+ skip_screenshots: false,
72
+ skip_metadata: false,
73
+ submit_for_review: true,
74
+ precheck_include_in_app_purchases: false,
75
+ reject_if_possible: true,
76
+ )
77
+ end
78
+ end
@@ -0,0 +1,5 @@
1
+ git_url("{{ios_match_git_url}}")
2
+
3
+ storage_mode("git")
4
+
5
+ app_identifier(["{{ios_app_identifier}}"])
@@ -0,0 +1,30 @@
1
+ ## Screenshots Naming Rules
2
+
3
+ Put all screenshots you want to use inside the folder of its language (e.g. `en-US`).
4
+ The device type will automatically be recognized using the image resolution.
5
+
6
+ The screenshots can be named whatever you want, but keep in mind they are sorted
7
+ alphabetically, in a human-friendly way. See https://github.com/fastlane/fastlane/pull/18200 for more details.
8
+
9
+ ### Exceptions
10
+
11
+ #### iPad Pro (3rd Gen) 12.9"
12
+
13
+ Since iPad Pro (3rd Gen) 12.9" and iPad Pro (2nd Gen) 12.9" have the same image
14
+ resolution, screenshots of the iPad Pro (3rd gen) 12.9" must contain either the
15
+ string `iPad Pro (12.9-inch) (3rd generation)`, `IPAD_PRO_3GEN_129`, or `ipadPro129`
16
+ (App Store Connect's internal naming of the display family for the 3rd generation iPad Pro)
17
+ in its filename to be assigned the correct display family and to be uploaded to
18
+ the correct screenshot slot in your app's metadata.
19
+
20
+ ### Other Platforms
21
+
22
+ #### Apple TV
23
+
24
+ Apple TV screenshots should be stored in a subdirectory named `appleTV` with language
25
+ folders inside of it.
26
+
27
+ #### iMessage
28
+
29
+ iMessage screenshots, like the Apple TV ones, should also be stored in a subdirectory
30
+ named `iMessage`, with language folders inside of it.
File without changes
@@ -0,0 +1,2 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ -----END CERTIFICATE-----
@@ -0,0 +1,3 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ ...
3
+ -----END PRIVATE KEY-----