Compare commits

...
44 Commits
Author SHA1 Message Date
Dominik Sokal 34deafe440 v1.0.0 2022-08-22 09:55:27 +02:00
Dominik Sokal 55d8b650d3 remove timeout when waiting for build / submit (#1289)
* remove timeout when waiting for build / submit

* update CHANGELOG.md
2022-08-22 09:43:57 +02:00
Will Schurman 3ef6802d55 [eas-cli] Limit the number of SignedAssetUploadSpecifications fetched at a time (#1287) 2022-08-19 10:11:26 -07:00
Dominik Sokal e9484ffa5a update CODEOWNERS 2022-08-19 13:51:38 +02:00
Will Schurman c2b50a3a08 [eas-cli] Remove git merge artifact from CHANGELOG (#1285) 2022-08-17 11:31:02 -07:00
Will Schurman 06bb202bdf [eas-cli] Improve reliability of update asset presigned upload requests (#1278) 2022-08-17 10:59:18 -07:00
Will Schurman 64b67e9ea7 [eas-cli] Configure EAS project ID in app config in all cases (#1282) 2022-08-17 10:12:14 -07:00
faf1e90bed [eas-cli] Add js metadata config file support (#1270)
* [eas-cli] Add js metadata config file support

* [eas-cli] Add js metadata config file download support

* update CHANGELOG.md

* [eas-cli] Only save static files when pulling metadata

* [eas-cli] Support config functions and async functions in metadata

* [eas-cli] Tweak the metadata logs

* [eas-cli] Improve the dynamic config return validation

* [eas-cli] Apply improved language suggestions from the pros

Co-authored-by: Dominik Sokal <[email protected]>

* [eas-cli] Apply code changes from review

Co-authored-by: Dominik Sokal <[email protected]>
Co-authored-by: Wojciech Kozyra <[email protected]>

* [eas-cli] Split out load config logging and move validation to single method

* [eas-cli] Fix missed renamed function

Co-authored-by: Dominik Sokal <[email protected]>
Co-authored-by: Wojciech Kozyra <[email protected]>
2022-08-17 16:16:52 +02:00
c8935e199b [workspace] improve DX while working on Windows (#1272)
Co-authored-by: Dominik Sokal <[email protected]>
Co-authored-by: Wojciech Kozyra <[email protected]>
2022-08-16 15:10:55 +02:00
Wojciech Kozyra af76a9ce48 [eas-cli] remove shell check (#1281) 2022-08-16 13:54:22 +02:00
Wojciech Kozyra 3986e2fbb7 [eas-cli] remove install script (#1280)
* [eas-cli] remove installer

* update CHANGELOG.md
2022-08-16 13:27:13 +02:00
Dominik Sokal 1f97e8cc1c [eas-cli] init git repo in monorepo root (#1279)
* [eas-cli] init git repo in monorepo root

* update CHANGELOG.md
2022-08-16 11:02:05 +02:00
dependabot[bot] be1e2d7189 Bump simple-plist from 1.1.1 to 1.3.1 (#1139)
Bumps [simple-plist](https://github.com/wollardj/simple-plist) from 1.1.1 to 1.3.1.
- [Release notes](https://github.com/wollardj/simple-plist/releases)
- [Commits](https://github.com/wollardj/simple-plist/compare/v1.1.1...v1.3.1)

---
updated-dependencies:
- dependency-name: simple-plist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-16 10:13:25 +02:00
Evan Bacon 8c4d7a6d0e fix: log binding was incorrect after ora instance (#1113)
* fix: log binding was incorrect after ora instance

* update CHANGELOG.md
2022-08-15 12:50:17 +02:00
Bartosz Kaszubowski 0849bf80b0 [workspace] bump Node used by oclif (#1273) 2022-08-12 15:20:40 +02:00
Expo CI 57ea4cd8c2 update CHANGELOG.md 2022-08-12 11:43:54 +00:00
Wojciech Kozyra 4c95900299 v0.60.0 2022-08-12 13:33:40 +02:00
Wojciech KozyraandDominik Sokal 94238f3274 [eas-cli] Add warning about unsupported policy. (#1271)
* [eas-cli] Add warning about unsupported policy.

* Update packages/eas-cli/src/project/projectUtils.ts

Co-authored-by: Dominik Sokal <[email protected]>

* update CHANGELOG.md

Co-authored-by: Dominik Sokal <[email protected]>
2022-08-12 13:20:06 +02:00
Wojciech Kozyra 361230ffd2 [eas-cli] add appVersion runtime policy (#1267)
* Update packages

* update CHANGELOG.md
2022-08-12 11:33:49 +02:00
Cedric van Putten 88065c2837 [eas-json] Add metadata path to ios submit profile (#1269)
* [eas-json] Add metadata path to ios submit profile

* update CHANGELOG.md
2022-08-12 10:19:33 +02:00
dependabot[bot] 7f7b59ccfc Bump ejs from 3.1.6 to 3.1.8 (#1125)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.8.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.8)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-08-11 12:14:08 +02:00
Expo CI 2a9f9d4695 update CHANGELOG.md 2022-08-10 10:15:59 +00:00
Dominik Sokal 8281a68e2e v0.59.0 2022-08-10 12:03:38 +02:00
Dominik Sokal cf47843712 upgrade dependencies (#1262)
* upgrade dependencies

* regenerate graphql code

* install ts-node

* update CHANGELOG.md
2022-08-10 11:54:17 +02:00
Wojciech Kozyra 7901fad195 yarn lint 2022-08-10 10:45:57 +02:00
Wojciech Kozyra df346764c8 [eas-cli] Validate only if using remote version source (#1261)
* [eas-cli] Validate only if using remote version source

* update CHANGELOG.md
2022-08-10 10:36:42 +02:00
Wojciech Kozyra 8024d33b25 [eas-cli] Improve wording for version bumps (#1260)
* [eas-cli] Improve wording for version bumps

* update CHANGELOG.md
2022-08-10 10:31:40 +02:00
Dominik Sokal e41d9f6430 Update CHANGELOG.md 2022-08-09 15:59:36 +02:00
Dominik Sokal 822313e90e upgrade eas-cli-local-build-plugin to 0.0.99 2022-08-09 15:56:40 +02:00
Expo CI 6c4f4c0049 update CHANGELOG.md 2022-08-09 11:44:42 +00:00
Dominik Sokal bb249345a7 v0.58.0 2022-08-09 13:31:08 +02:00
Dominik Sokal c1716e3b5a Update CHANGELOG.md 2022-08-09 12:11:05 +02:00
Dominik Sokal 40a6f3dda3 [ENG-3865] pass build metadata to local plugin (#1256)
* pass build metadata to local plugin

* upgrade eas-cli-local-build-plugin to 0.0.98
2022-08-09 12:05:04 +02:00
Wojciech Kozyra 8cadda2099 [eas-cli] add submit info to json only output (#1246)
* add submit info to json only output

* review feedback

* update CHANGELOG.md
2022-08-09 11:09:44 +02:00
Brent Vatne 8837214db2 [eas-cli] Swallow error when unable to track file in no commit workflow (#1109)
* [eas-cli] Swallow error when unable to track file in no commit workflow

* Include a warning when failing to track a file with GitNoCommitClient

* update CHANGELOG.md
2022-08-06 16:34:22 -07:00
kirb 1812b85e39 [eas-cli] add asset limit warning on publish flow (#1243) 2022-08-05 11:39:00 -07:00
Cedric van Putten 5024164408 [eas-cli] Add version selector to store config for metadata (#1194)
* [eas-cli] Add version selector to store config for metadata

* [eas-cli] Add version snippet to emphasize on app version

* [eas-cli] Add the missing phased release nock endpoints
2022-08-04 07:53:24 -07:00
Wojciech Kozyra b7983f4998 [eas-json] stop including sources in the npm package (#1248)
* [eas-json] Stop including sources in npm package

* update CHANGELOG.md
2022-08-04 15:43:00 +02:00
Wojciech Kozyra 7eea81ef83 [eas-json] move eas.json schema to eas-cli repo (#1247) 2022-08-04 15:05:05 +02:00
Cedric van Putten 5b9b4f9a2b [eas-cli] Finalize the store config release properties (#1191)
* [eas-cli] Clean up the store config release options

* [eas-cli] Add basic implementation of phased release

* [eas-cli] Fix outdated config writer tests
2022-08-04 03:04:09 -07:00
Cedric van Putten f862b7744b [eas-cli] Remove eas metadata preview until implementation is done (#1195) 2022-08-04 02:39:55 -07:00
Dominik Sokal e0b38f79dc [ENG-5893] add runWithNoWaitFlag and runFromCI to build metadata (#1245)
* generate graphql code

* add runWithNoWaitFlag and runFromCI to metadata

* send analytics events

* address PR feedback

* address more PR feedback

* upgrade @expo/eas-build-job
2022-08-04 09:52:00 +02:00
Wojciech Kozyra a709b9fd23 [eas-cli] managed versions support (#1232) 2022-08-03 12:52:26 +02:00
Expo CI ad2109103a update CHANGELOG.md 2022-08-03 07:48:35 +00:00
90 changed files with 6644 additions and 4071 deletions

No files matched your search

+1
View File
@@ -1 +1,2 @@
* text eol=lf
**/yarn.lock linguist-generated
-20
View File
@@ -1,20 +0,0 @@
name: Installer tests
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
name: Run the installer
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- run: bash bin/install.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: eas --version
-1
View File
@@ -32,7 +32,6 @@ jobs:
# Remove this whenever we solved those OOM errors.
NODE_OPTIONS: --max-old-space-size=4096
- run: yarn lint --max-warnings=0
- run: shellcheck bin/*.sh
- uses: codecov/codecov-action@v1
if: ${{ matrix.coverage }}
with:
+2 -1
View File
@@ -4,5 +4,6 @@
"singleQuote": true,
"bracketSameLine": true,
"trailingComma": "es5",
"arrowParens": "avoid"
"arrowParens": "avoid",
"endOfLine": "auto"
}
+56 -4
View File
@@ -6,16 +6,68 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes
- Remove timeout when waiting for build / submission. ([#1289](https://github.com/expo/eas-cli/pull/1289) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add metadata support for dynamic store.config.js files. ([#1270](https://github.com/expo/eas-cli/pull/1270) by [@byCedric](https://github.com/byCedric))
- Improve reliability of update asset presigned upload requests. ([#1278](https://github.com/expo/eas-cli/pull/1278) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Rebind `console.info` correctly after `ora` instance stops. ([#1113](https://github.com/expo/eas-cli/pull/1113) by [@EvanBacon](https://github.com/EvanBacon))
- Fix initializing git repository in monorepo. ([#1279](https://github.com/expo/eas-cli/pull/1279) by [@dsokal](https://github.com/dsokal))
- Limit the number of SignedAssetUploadSpecifications fetched at a time. ([#1287](https://github.com/expo/eas-cli/pull/1287) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unused install script. ([#1280](https://github.com/expo/eas-cli/pull/1280), [#1281](https://github.com/expo/eas-cli/pull/1281) by [@wkozyra95](https://github.com/wkozyra95))
## [0.60.0](https://github.com/expo/eas-cli/releases/tag/v0.60.0) - 2022-08-12
### 🎉 New features
- Add `appVersion` runtime policy. ([#1267](https://github.com/expo/eas-cli/pull/1267) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add JSON Schema `metadataPath` to iOS submission profile. ([#1269](https://github.com/expo/eas-cli/pull/1269) by [@byCedric](https://github.com/byCedric))
- Add warning when `appVersion` runtime policy is not supported. ([#1271](https://github.com/expo/eas-cli/pull/1271) by [@wkozyra95](https://github.com/wkozyra95))
## [0.59.0](https://github.com/expo/eas-cli/releases/tag/v0.59.0) - 2022-08-10
### 🐛 Bug fixes
- Fix building Android projects locally that don't have execute permissions set for `gradlew`. ([82231](https://github.com/expo/eas-cli/commit/822313e90e94bd6ddd3872061a0c25a8aa4db7cd) by [@dsokal](https://github.com/dsokal))
- Disable `nativeVersion` policy only for remote version source. ([#1261](https://github.com/expo/eas-cli/pull/1261) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Improve wording on version auto-increment. ([#1260](https://github.com/expo/eas-cli/pull/1260) by [@wkozyra95](https://github.com/wkozyra95))
- Upgrade dependencies. ([#1262](https://github.com/expo/eas-cli/pull/1262) by [@dsokal](https://github.com/dsokal))
## [0.58.0](https://github.com/expo/eas-cli/releases/tag/v0.58.0) - 2022-08-09
### 🎉 New features
- Managed app versions support. ([#1209](https://github.com/expo/eas-cli/pull/1209), [#1219](https://github.com/expo/eas-cli/pull/1219), [#1232](https://github.com/expo/eas-cli/pull/1232) by [@wkozyra95](https://github.com/wkozyra95))
- Add a warning when publishing an update with too many assets. ([#1243](https://github.com/expo/eas-cli/pull/1243) by [@kgc00](https://github.com/kgc00))
- Add submission info when building with `--json` option. ([#1246](https://github.com/expo/eas-cli/pull/1246) by [@wkozyra95](https://github.com/wkozyra95))
- Set all environment variables (documented in https://docs.expo.dev/build-reference/variables/) when running local build. ([#1256](https://github.com/expo/eas-cli/pull/1256) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Do not include sources in `eas-json` npm package. ([#1248](https://github.com/expo/eas-cli/pull/1248) by [@wkozyra95](https://github.com/wkozyra95))
- Swallow error when unable to track file in no commit workflow. ([#1109](https://github.com/expo/eas-cli/pull/1109) by [@brentvatne](https://github.com/brentvatne))
## [0.57.0](https://github.com/expo/eas-cli/releases/tag/v0.57.0) - 2022-08-03
### 🎉 New features
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
### 🐛 Bug fixes
### 🧹 Chores
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
### 🎉 New features
+2 -2
View File
@@ -3,8 +3,8 @@
packages/eas-cli/src/build @dsokal @wkozyra95
packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
packages/eas-cli/src/submit @dsokal
packages/eas-cli/src/commands/submit.ts @dsokal
packages/eas-cli/src/update @kgc00 @wschurman
packages/eas-cli/src/commands/update @kgc00 @wschurman
-130
View File
@@ -1,130 +0,0 @@
#!/bin/bash
# This script downloads the latest release version of EAS CLI from GitHub and
# installs it in /usr/local/bin
{
# All code is inside a block to ensure the script executes only when
# downloaded completely.
set -euo pipefail
prefix="${EAS_PREFIX:-/usr/local}"
temp_dir="$(mktemp -d)"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
then
# Use colors when stdout is a terminal and NO_COLOR isn't set.
text_bold=$'\033[1m'
text_normal=$'\033[22m'
text_red=$'\033[31m'
text_default_color=$'\033[39m'
else
# Disable colors
text_bold=""
text_normal=""
text_red=""
text_default_color=""
fi
abort() {
echo -e "$text_bold$text_red$1$text_normal$text_default_color"
exit 1
}
install() {
needs_sudo=""
# Make sure path exists and can be written to by the current user.
if ! mkdir -p "$prefix/lib" || [[ ! -w "$prefix/lib" ]]
then
needs_sudo=true
fi
if ! mkdir -p "$prefix/bin" || [[ ! -w "$prefix/bin" ]]
then
needs_sudo=true
fi
if ! rm -rf "$prefix/lib/eas"
then
needs_sudo=true
fi
# Run commands with sudo if necessary.
if [ -n "$needs_sudo" ]
then
if [ "$EUID" -ne 0 ]
then
echo "Installing requires superuser access."
echo "The sudo command will prompt for your password."
fi
sudo rm -rf "$prefix/lib/eas"
sudo mv "$temp_dir/eas" "$prefix/lib/eas"
sudo ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
else
mv "$temp_dir/eas" "$prefix/lib/eas"
ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
fi
}
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
fi
hardware="$(uname -m)"
case "$hardware" in
x86_64)
arch="x64"
;;
arm*)
arch="arm"
;;
*)
abort "Unsupported CPU architecture $hardware."
;;
esac
case "$(uname -s)" in
Darwin*)
platform="darwin"
if [[ "$arch" != "x64" ]]; then
abort "Unsupported CPU architecture $hardware on macOS."
fi
;;
Linux*)
platform="linux"
;;
*)
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
;;
esac
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
echo "Installing EAS CLI from $url"
mkdir -p "$temp_dir"
cd "$temp_dir"
echo
echo "Downloading..."
if [ "$(command -v curl)" ]
then
curl --location --progress-bar "$url" | tar xz
else
wget -O- "$url" | tar xz
fi
# Move the installation over and link the executable (may require sudo)
install
# Test the bundled node binary and if it doesn't work remove it (fall back to
# node installed in path).
"$prefix/lib/eas/bin/node" -v > /dev/null || {
rm "$prefix/lib/eas/bin/node"
}
# Test the CLI
executable=$(command -v eas) || abort "Could not run eas after installation."
echo
echo -e "EAS CLI is installed to ${executable}"
eas --version
echo
echo -e "Type ${text_bold}eas --help${text_normal} to get started."
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "0.57.0"
"version": "1.0.0"
}
+10 -10
View File
@@ -17,18 +17,18 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "27.5.0",
"@types/node": "17.0.32",
"eslint": "8.15.0",
"eslint-config-universe": "11.0.0",
"eslint-plugin-async-protect": "1.1.0",
"@types/jest": "28.1.6",
"@types/node": "18.6.5",
"eslint": "8.21.0",
"eslint-config-universe": "11.1.0",
"eslint-plugin-async-protect": "2.1.0",
"eslint-plugin-node": "11.1.0",
"jest": "28.1.0",
"jest-watch-typeahead": "1.1.0",
"jest": "28.1.3",
"jest-watch-typeahead": "2.0.0",
"lerna": "4.0.0",
"oclif": "3.0.1",
"prettier": "2.6.2",
"ts-jest": "28.0.2",
"oclif": "3.1.2",
"prettier": "2.7.1",
"ts-jest": "28.0.7",
"typescript": "4.7.4"
},
"volta": {
+84 -44
View File
@@ -49,6 +49,8 @@ eas --help COMMAND
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:submit`](#eas-buildsubmit)
* [`eas build:version:set`](#eas-buildversionset)
* [`eas build:version:sync`](#eas-buildversionsync)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
@@ -100,7 +102,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -117,7 +119,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -134,7 +136,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -148,7 +150,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -197,7 +199,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -217,7 +219,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -238,7 +240,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -257,7 +259,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -281,7 +283,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -311,7 +313,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -325,7 +327,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -342,7 +344,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -377,7 +379,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -410,7 +412,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:submit`
@@ -440,6 +442,44 @@ ALIASES
$ eas build:submit
```
## `eas build:version:set`
Update version of an app.
```
USAGE
$ eas build:version:set [-p android|ios] [--profile <value>]
FLAGS
-p, --platform=(android|ios)
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
Update a version in native code with a value stored on EAS servers
```
USAGE
$ eas build:version:sync [-p android|ios|all] [--profile <value>]
FLAGS
-p, --platform=(android|ios|all)
--profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
eas.json.
DESCRIPTION
Update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
view a build for your project
@@ -455,7 +495,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -475,7 +515,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -496,7 +536,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -513,7 +553,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -533,7 +573,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -551,7 +591,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -565,7 +605,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -579,7 +619,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -597,7 +637,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -614,7 +654,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -628,7 +668,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -642,7 +682,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -724,7 +764,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -741,7 +781,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas project:info`
@@ -755,7 +795,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -772,7 +812,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -792,7 +832,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -809,7 +849,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -823,7 +863,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas submit`
@@ -853,7 +893,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -883,7 +923,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -900,7 +940,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -920,7 +960,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -939,7 +979,7 @@ DESCRIPTION
view the recent updates for a branch
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:view GROUPID`
@@ -959,7 +999,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -979,7 +1019,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -996,7 +1036,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1013,7 +1053,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1034,7 +1074,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1051,7 +1091,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.57.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v1.0.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+1 -1
View File
@@ -14,7 +14,7 @@ generates:
dedupeOperationSuffix: true
hooks:
afterOneFileWrite:
- ./annotate-graphql-codegen.sh
- 'node ./scripts/annotate-graphql-codegen.js'
./graphql.schema.json:
plugins:
- 'introspection'
+2224 -1645
View File
File diff suppressed because it is too large. Load diff
+33 -28
View File
@@ -1,7 +1,7 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "0.57.0",
"version": "1.0.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
@@ -10,29 +10,30 @@
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.33",
"@expo/code-signing-certificates": "0.0.2",
"@expo/config": "6.0.23",
"@expo/config-plugins": "4.1.4",
"@expo/config-types": "45.0.0",
"@expo/eas-build-job": "0.2.84",
"@expo/eas-json": "0.57.0",
"@expo/config": "7.0.1",
"@expo/config-plugins": "5.0.1",
"@expo/config-types": "46.0.2",
"@expo/eas-build-job": "0.2.85",
"@expo/eas-json": "1.0.0",
"@expo/json-file": "8.2.36",
"@expo/multipart-body-parser": "1.1.0",
"@expo/package-manager": "0.0.56",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.18",
"@expo/plugin-autocomplete": "1.4.0",
"@expo/plugin-help": "5.2.0",
"@expo/plugin-warn-if-update-available": "2.3.0",
"@expo/prebuild-config": "3.1.6",
"@expo/prebuild-config": "5.0.3",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.6.0",
"@expo/spawn-async": "1.7.0",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.9.0",
"@urql/core": "2.4.4",
"@oclif/core": "1.13.10",
"@urql/core": "2.6.1",
"@urql/exchange-retry": "0.3.3",
"ajv": "^6.12.6",
"chalk": "4.1.2",
"cli-progress": "3.11.0",
"cli-progress": "3.11.2",
"cli-table3": "0.6.2",
"dateformat": "4.6.3",
"env-paths": "2.2.0",
@@ -54,7 +55,7 @@
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.0.1",
"minimatch": "5.1.0",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
@@ -62,6 +63,7 @@
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"promise-limit": "2.7.0",
"promise-retry": "2.0.1",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
@@ -77,33 +79,36 @@
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.4.11",
"@graphql-codegen/typescript-operations": "2.4.0",
"@types/cli-progress": "3.9.2",
"@graphql-codegen/cli": "2.11.5",
"@graphql-codegen/introspection": "2.2.1",
"@graphql-codegen/typescript": "2.7.3",
"@graphql-codegen/typescript-operations": "2.5.3",
"@types/cli-progress": "3.11.0",
"@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/mime": "2.0.3",
"@types/node-fetch": "^2.5.7",
"@types/node-forge": "1.0.2",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.0.4",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.9",
"@types/tar": "6.1.1",
"@types/semver": "7.3.10",
"@types/tar": "6.1.2",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "8.3.4",
"@types/wrap-ansi": "3.0.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"form-data": "4.0.0",
"memfs": "3.4.3",
"memfs": "3.4.7",
"mockdate": "3.0.5",
"nock": "13.2.4",
"nock": "13.2.9",
"rimraf": "3.0.2",
"tempy": "0.7.0",
"ts-deepmerge": "2.0.1",
"ts-deepmerge": "2.0.5",
"ts-mockito": "2.6.1",
"ts-node": "10.9.1",
"typescript": "4.7.4"
},
"engines": {
@@ -171,7 +176,7 @@
},
"update": {
"node": {
"version": "12.13.0"
"version": "18.6.0"
},
"s3": {
"templates": {
@@ -191,8 +196,8 @@
},
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rm -f oclif.manifest.json",
"prepack": "./scripts/prepack.sh",
"postpack": "rimraf oclif.manifest.json",
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
@@ -200,7 +205,7 @@
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rm -rf dist build tmp node_modules yarn-error.log"
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
"node": "18.6.0",
+39 -234
View File
@@ -3,21 +3,6 @@
"type": "object",
"definitions": {
"apple": {
"AppleLocalizedPreviewFolder": {
"anyOf": [
{
"description": "Path to folder containing preview files",
"type": "string"
},
{
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
}
}
]
},
"AppleKidsAge": {
"enum": [
"FIVE_AND_UNDER",
@@ -464,62 +449,43 @@
},
"AppleRelease": {
"type": "object",
"description": "Configure how the app is released in the app store.",
"additionalProperties": false,
"defaultSnippets": [
{
"body": {
"automaticRelease": true,
"usesThirdPartyContent": false,
"usesNonExemptEncryption": false
}
}
],
"properties": {
"autoReleaseDate": {
"description": "ISO Date to release the app automatically. Leave undefined to manually release the app to the public.",
"defaultSnippets": [
{
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}"
}
],
"anyOf": [
"automaticRelease": {
"description": "If and how the app should automatically be released when approved by App Store review.",
"markdownDescription": "If and how the app should automatically be released when approved by App Store review.\n- `false` - Manually release the app after store approval. (default behavior)\n- `true` - Automatically release after store approval.\n- `Date` - Automatically schedule release on this date, after store approval.\n\nApple does not guarantee that your app is available at the chosen scheduled release date.",
"oneOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "string",
"format": "date"
"type": "boolean"
}
],
"defaultSnippets": [
{
"label": "Manual release",
"description": "Manually release the app after store approval",
"body": false
},
{
"type": "number"
"label": "Automatic release",
"description": "Automatically release the app after store approval",
"body": true
},
{
"label": "Scheduled release",
"description": "Automatically release the app on this date, and store approval",
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}T${3:$CURRENT_HOUR}:00:00+00:00"
}
]
},
"isPhasedReleaseEnabled": {
"phasedRelease": {
"type": "boolean",
"description": "Release update over 7-day period instead of instantly.",
"meta": {
"storeInfo": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time. [Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)."
}
},
"shouldResetRatings": {
"type": "boolean",
"description": "Should App Store ratings be reset for this version of the app.",
"meta": {
"storeInfo": "You can reset your app’s summary rating for all countries or regions when you release this version. Keep in mind that once this version is released, you won’t be able to restore the rating. Your app’s existing customer reviews will still appear on the App Store."
}
},
"usesThirdPartyContent": {
"type": "boolean"
},
"automaticRelease": {
"type": "boolean",
"description": "Should the app automatically be released when approved by App Store review."
},
"usesNonExemptEncryption": {
"type": "boolean",
"description": "Alternative to setting `ITSAppUsesNonExemptEncryption` in the binary's `Info.plist`."
"description": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time.\n@see https://help.apple.com/app-store-connect/#/dev3d65fcee1",
"markdownDescription": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates.\n\nKeep in mind that this version will still be available to all users as a manual update from the App Store.\n\nYou can pause the phased release for up to 30 days or release this update to all users at any time.\n\n[Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)"
}
}
},
@@ -606,11 +572,15 @@
"description": "Configuration that is specific to the Apple App Store.",
"additionalProperties": false,
"properties": {
"advisory": {
"$ref": "#/definitions/apple/AppleAdvisory"
},
"categories": {
"$ref": "#/definitions/apple/AppleCategory"
"version": {
"type": "string",
"description": "The version string to edit, or create, if it doesn't exist. All the metadata will be stored on this app version.",
"minLength": 1,
"defaultSnippets": [
{
"body": "${1:1}.${2:0}"
}
]
},
"copyright": {
"type": "string",
@@ -626,6 +596,12 @@
"storeInfo": "The name of the person or entity that owns the exclusive rights to your app, preceded by the year the rights were obtained (for example, \"2008 Acme Inc.\"). Do not provide a URL."
}
},
"advisory": {
"$ref": "#/definitions/apple/AppleAdvisory"
},
"categories": {
"$ref": "#/definitions/apple/AppleCategory"
},
"info": {
"description": "Localized core app info",
"type": "object",
@@ -789,177 +765,6 @@
}
}
},
"preview": {
"anyOf": [
{
"description": "Path to localized folders containing preview files",
"type": "string"
},
{
"description": "Localized previews object. Ex: `\"en-US\": [\"image.png\"]`",
"type": "object",
"additionalProperties": false,
"properties": {
"ar-SA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Arabic"
},
"ca": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Catalan"
},
"zh-Hans": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Chinese (Simplified)"
},
"zh-Hant": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Chinese (Traditional)"
},
"hr": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Croatian"
},
"cs": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Czech"
},
"da": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Danish"
},
"nl-NL": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Dutch"
},
"en-AU": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (Australia)"
},
"en-CA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (Canada)"
},
"en-GB": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (U.K.)"
},
"en-US": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "English (U.S.)"
},
"fi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Finnish"
},
"fr-FR": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "French"
},
"fr-CA": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "French (Canada)"
},
"de-DE": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "German"
},
"el": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Greek"
},
"he": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hebrew"
},
"hi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hindi"
},
"hu": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Hungarian"
},
"id": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Indonesian"
},
"it": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Italian"
},
"ja": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Japanese"
},
"ko": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Korean"
},
"ms": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Malay"
},
"no": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Norwegian"
},
"pl": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Polish"
},
"pt-BR": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Portuguese (Brazil)"
},
"pt-PT": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Portuguese (Portugal)"
},
"ro": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Romanian"
},
"ru": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Russian"
},
"sk": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Slovak"
},
"es-MX": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Spanish (Mexico)"
},
"es-ES": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Spanish (Spain)"
},
"sv": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Swedish"
},
"th": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Thai"
},
"tr": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Turkish"
},
"uk": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Ukrainian"
},
"vi": {
"$ref": "#/definitions/apple/AppleLocalizedPreviewFolder",
"description": "Vietnamese"
}
}
}
]
},
"release": {
"$ref": "#/definitions/apple/AppleRelease",
"description": "Release strategy"
@@ -1,13 +1,15 @@
#!/usr/bin/env sh
const fs = require('fs/promises');
OUTFILE="src/graphql/generated.ts"
const path = './src/graphql/generated.ts';
content="/**
(async () => {
const generatedCode = await fs.readFile(path, 'utf8');
const comment = `/**
* This file was generated using GraphQL Codegen
* Command: yarn generate-graphql-code
* Run this during development for automatic type generation when editing GraphQL documents
* For more info and docs, visit https://graphql-code-generator.com/
*/
"
*/\n\n`;
printf '%s\n%s\n' "$content" "$(cat $OUTFILE)" > $OUTFILE
await fs.writeFile(path, comment + generatedCode);
})();
+1 -1
View File
@@ -1,4 +1,4 @@
const { promises: fs } = require('fs');
const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
+7
View File
@@ -0,0 +1,7 @@
const spawn = require('@expo/spawn-async');
(async () => {
if (!process.env.CLI_SIZE_CHECK) {
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
}
})();
-10
View File
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
rm -rf build
yarn build
if [ -z "$CLI_SIZE_CHECK" ]; then
yarn oclif manifest
fi
+17 -2
View File
@@ -1,4 +1,5 @@
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -28,6 +29,7 @@ import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate'
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
@@ -60,8 +62,18 @@ This means that it will most likely produce an AAB and you will not be able to i
}
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
const versionCodeOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteVersionCodeAsync({
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationId,
buildProfile,
})
: undefined;
return { applicationId, gradleContext };
return { applicationId, gradleContext, versionCodeOverride };
}
export async function prepareAndroidBuildAsync(
@@ -76,7 +88,10 @@ export async function prepareAndroidBuildAsync(
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
});
},
prepareJobAsync: async (
@@ -13,6 +13,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
gradleCommand: job.gradleCommand,
artifactPath: job.artifactPath,
username: job.username,
@@ -75,6 +75,11 @@ export async function prepareJobAsync(
artifactPath: buildProfile.artifactPath,
buildType,
username,
...(ctx.android.versionCodeOverride && {
version: {
versionCode: ctx.android.versionCodeOverride,
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
@@ -1,7 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
@@ -15,15 +15,14 @@ import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './ver
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
localAutoIncrement,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.ANDROID>;
localAutoIncrement?: AndroidVersionAutoIncrement;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
+99 -1
View File
@@ -1,11 +1,15 @@
import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins';
import { AndroidConfig, Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { ora } from '../../ora';
import {
getAppBuildGradleAsync,
parseGradleCommand,
@@ -165,3 +169,97 @@ async function writeBuildGradleAsync({
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle);
}
/**
* Returns buildNumber that will be used for the next build. If current build profile
* has an 'autoIncrement' option set, it increments the version on server.
*/
export async function resolveRemoteVersionCodeAsync({
projectDir,
projectId,
exp,
applicationId,
buildProfile,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationId: string;
buildProfile: BuildProfile<Platform.ANDROID>;
}): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
projectId,
AppPlatform.Android,
applicationId
);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localVersions.appBuildVersion) {
Log.warn(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
);
currentBuildVersion = localVersions.appBuildVersion;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
);
throw new Error('Remote versions are not configured.');
}
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync({
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
spinner.fail(`Failed to initialize versionCode with ${chalk.bold(currentBuildVersion)}.`);
throw err;
}
return currentBuildVersion;
} else {
const nextBuildVersion = getNextVersionCode(currentBuildVersion);
const spinner = ora(
`Incrementing versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync({
appId: projectId,
platform: AppPlatform.Android,
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
} catch (err) {
spinner.fail(
`Failed to increment versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
throw err;
}
return String(nextBuildVersion);
}
}
+3 -12
View File
@@ -120,7 +120,7 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => {
if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, ctx.localBuildOptions);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else {
try {
@@ -243,11 +243,7 @@ type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
// 2 hours (max build time limit) + 10 minutes (possible queue time)
timeoutSec = 2 * 60 * 60 + 10 * 60,
intervalSec = 10,
} = {}
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
let originalSpinnerText;
@@ -259,8 +255,7 @@ export async function waitForBuildEndAsync(
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
}
const endTime = new Date().getTime() + timeoutSec * 1000;
while (new Date().getTime() <= endTime) {
while (true) {
const builds = await getBuildsSafelyAsync(buildIds);
const { refetch } =
builds.length === 1
@@ -271,10 +266,6 @@ export async function waitForBuildEndAsync(
}
await sleepAsync(intervalSec * 1000);
}
spinner.fail('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
}
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
+4
View File
@@ -16,6 +16,7 @@ export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android'
export interface AndroidBuildContext {
applicationId: string;
gradleContext?: GradleBuildContext;
versionCodeOverride?: string;
}
export interface IosBuildContext {
@@ -23,6 +24,7 @@ export interface IosBuildContext {
applicationTarget: Target;
targets: Target[];
xcodeBuildContext: XcodeBuildContext;
buildNumberOverride?: string;
}
export interface BuildContext<T extends Platform> {
@@ -36,6 +38,8 @@ export interface BuildContext<T extends Platform> {
exp: ExpoConfig;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
noWait: boolean;
runFromCI: boolean;
platform: T;
projectDir: string;
projectId: string;
+9 -1
View File
@@ -1,6 +1,7 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
@@ -24,7 +25,8 @@ export async function createBuildContextAsync<T extends Platform>({
easJsonCliConfig,
clearCache = false,
localBuildOptions,
nonInteractive = false,
nonInteractive,
noWait,
platform,
projectDir,
resourceClass,
@@ -36,6 +38,7 @@ export async function createBuildContextAsync<T extends Platform>({
clearCache: boolean;
localBuildOptions: LocalBuildOptions;
nonInteractive: boolean;
noWait: boolean;
platform: T;
projectDir: string;
resourceClass: BuildResourceClass;
@@ -49,6 +52,7 @@ export async function createBuildContextAsync<T extends Platform>({
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = findAccountByName(user.accounts, accountName)?.id;
const runFromCI = getenv.boolish('CI', false);
const credentialsCtx = new CredentialsContext({
exp,
@@ -70,6 +74,8 @@ export async function createBuildContextAsync<T extends Platform>({
project_id: projectId,
project_type: workflow,
...devClientProperties,
no_wait: noWait,
run_from_ci: runFromCI,
};
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
@@ -84,6 +90,7 @@ export async function createBuildContextAsync<T extends Platform>({
exp,
localBuildOptions,
nonInteractive,
noWait,
platform,
projectDir,
projectId,
@@ -92,6 +99,7 @@ export async function createBuildContextAsync<T extends Platform>({
user,
workflow,
message,
runFromCI,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
+17 -2
View File
@@ -1,4 +1,5 @@
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
@@ -14,6 +15,7 @@ import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteBuildNumberAsync } from './version';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
@@ -42,12 +44,22 @@ export async function createIosContextAsync(
env: buildProfile.env,
});
const applicationTarget = findApplicationTarget(targets);
const buildNumberOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteBuildNumberAsync({
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationTarget,
buildProfile,
})
: undefined;
return {
bundleIdentifier: applicationTarget.bundleIdentifier,
applicationTarget,
targets,
xcodeBuildContext,
buildNumberOverride,
};
}
@@ -63,8 +75,11 @@ export async function prepareIosBuildAsync(
await syncProjectConfigurationAsync({
projectDir: ctx.projectDir,
exp: ctx.exp,
buildProfile: ctx.buildProfile,
targets: ctx.ios.targets,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
});
},
prepareJobAsync: async (
@@ -14,6 +14,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
scheme: job.scheme,
buildConfiguration: job.buildConfiguration,
artifactPath: job.artifactPath,
@@ -66,6 +66,11 @@ export async function prepareJobAsync(
buildConfiguration: ctx.buildProfile.buildConfiguration,
artifactPath: ctx.buildProfile.artifactPath,
username,
...(ctx.ios.buildNumberOverride && {
version: {
buildNumber: ctx.ios.buildNumberOverride,
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
@@ -1,6 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import { IosVersionAutoIncrement } from '@expo/eas-json';
import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
@@ -11,17 +11,16 @@ import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './ver
export async function syncProjectConfigurationAsync({
projectDir,
exp,
buildProfile,
targets,
localAutoIncrement,
}: {
projectDir: string;
exp: ExpoConfig;
buildProfile: BuildProfile<Platform.IOS>;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
+109 -1
View File
@@ -1,12 +1,17 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { IOSConfig, Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk';
import path from 'path';
import type { XCBuildConfiguration } from 'xcode';
import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
@@ -245,3 +250,106 @@ export function evaluateTemplateString(
}
});
}
/**
* Returns buildNumber that will be used for the next build. If current build profile
* has an 'autoIncrement' option set, it increments the version on server.
*/
export async function resolveRemoteBuildNumberAsync({
projectDir,
projectId,
exp,
applicationTarget,
buildProfile,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationTarget: Target;
buildProfile: BuildProfile<Platform.IOS>;
}): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
projectId,
AppPlatform.Ios,
applicationTarget.bundleIdentifier
);
const localBuildNumber = await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
);
const localShortVersion = await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localBuildNumber) {
Log.warn(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
);
currentBuildVersion = localBuildNumber;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
);
throw new Error('Remote versions are not configured.');
}
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync({
appId: projectId,
platform: AppPlatform.Ios,
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
spinner.fail(`Failed to initialize buildNumber with ${chalk.bold(currentBuildVersion)}.`);
throw err;
}
return currentBuildVersion;
} else {
const nextBuildVersion = getNextBuildNumber(currentBuildVersion);
const spinner = ora(
`Incrementing buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
).start();
try {
await AppVersionMutation.createAppVersionAsync({
appId: projectId,
platform: AppPlatform.Ios,
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: nextBuildVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
} catch (err) {
spinner.fail(
`Failed to increment buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
nextBuildVersion
)}.`
);
throw err;
}
return nextBuildVersion;
}
}
+15 -8
View File
@@ -1,4 +1,4 @@
import { Job } from '@expo/eas-build-job';
import { Job, Metadata } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.85';
const PLUGIN_PACKAGE_VERSION = '0.0.99';
export interface LocalBuildOptions {
enable: boolean;
@@ -18,8 +18,12 @@ export interface LocalBuildOptions {
verbose?: boolean;
}
export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job);
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
if (!options.verbose) {
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
@@ -53,15 +57,18 @@ export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions):
}
}
async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
async function getCommandAndArgsAsync(
job: Job,
metadata: Metadata
): Promise<{ command: string; args: string[] }> {
const jobAndMetadataBase64 = Buffer.from(JSON.stringify({ job, metadata })).toString('base64');
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobBase64],
args: [jobAndMetadataBase64],
};
} else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobAndMetadataBase64];
if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
// prompting the user to install a package that is used for the first time
+26 -2
View File
@@ -56,6 +56,8 @@ export async function collectMetadataAsync<T extends Platform>(
ctx as BuildContext<Platform.IOS>
),
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
};
return sanitizeMetadata(metadata);
}
@@ -65,10 +67,32 @@ async function maybeResolveVersionsAsync<T extends Platform>(
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) {
const iosContext = ctx as BuildContext<Platform.IOS>;
return await maybeResolveIosVersionsAsync(ctx.projectDir, ctx.exp, iosContext.ios.targets);
const resolvedVersion = await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext.ios.targets
);
if (iosContext.ios.buildNumberOverride) {
return {
...resolvedVersion,
appBuildVersion: iosContext.ios.buildNumberOverride,
};
}
return resolvedVersion;
} else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
ctx.projectDir,
ctx.exp,
androidCtx.buildProfile
);
if (androidCtx.android.versionCodeOverride) {
return {
...resolvedVersion,
appBuildVersion: androidCtx.android.versionCodeOverride,
};
}
return resolvedVersion;
} else {
throw new Error(`Unsupported platform ${ctx.platform}`);
}
@@ -1,5 +1,11 @@
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson, EasJsonReader, SubmitProfile } from '@expo/eas-json';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonReader,
SubmitProfile,
} from '@expo/eas-json';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
@@ -8,8 +14,10 @@ import {
BuildFragment,
BuildResourceClass,
BuildStatus,
BuildWithSubmissionsFragment,
SubmissionFragment,
} from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log';
import {
@@ -20,8 +28,14 @@ import {
} from '../platform';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
import {
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { createSubmissionContextAsync } from '../submit/context';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
@@ -97,8 +111,12 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
buildProfiles,
});
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
}
const startedBuilds: {
build: BuildFragment;
build: BuildWithSubmissionsFragment | BuildFragment;
buildProfile: ProfileData<'build'>;
}[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
@@ -152,6 +170,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
submitProfile,
nonInteractive: flags.nonInteractive,
});
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(startedBuild.build.id);
submissions.push(submission);
}
@@ -172,16 +191,30 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
});
printBuildResults(builds, flags.json);
if (!flags.json) {
printBuildResults(builds);
}
const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
printJsonOnlyOutput(builds);
}
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else {
// the following function also exits with non zero code if any of the submissions failed
await waitForSubmissionsToCompleteAsync(submissions);
const completedSubmissions = await waitForSubmissionsToCompleteAsync(submissions);
if (flags.json) {
printJsonOnlyOutput(
await Promise.all(
builds
.filter((i): i is BuildWithSubmissionsFragment => !!i)
.map(build => BuildQuery.withSubmissionsByIdAsync(build.id))
)
);
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
}
@@ -206,6 +239,7 @@ async function prepareAndStartBuildAsync({
clearCache: flags.clearCache,
buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive,
noWait: !flags.wait,
platform: buildProfile.platform,
projectDir,
localBuildOptions: flags.localBuildOptions,
@@ -222,7 +256,10 @@ async function prepareAndStartBuildAsync({
)}`
);
}
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
}
if (buildCtx.workflow === Workflow.MANAGED) {
if (!sdkVersionChecked) {
await checkExpoSdkIsSupportedAsync(buildCtx);
@@ -14,7 +14,6 @@ import {
} from '../../graphql/generated';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
export function printLogsUrls(builds: BuildFragment[]): void {
@@ -32,18 +31,14 @@ export function printLogsUrls(builds: BuildFragment[]): void {
}
}
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (json) {
printJsonOnlyOutput(builds);
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
@@ -20,15 +20,14 @@ import {
getProjectIdAsync,
} from '../../../project/projectUtils';
import {
ensureRemoteVersionPolicyAsync,
ensureVersionSourceIsRemoteAsync,
getBuildVersionName,
validateAppConfigForRemoteVersionPolicyAsync,
validateAppConfigForRemoteVersionSource,
} from '../../../project/remoteVersionSource';
import { promptAsync } from '../../../prompts';
export default class BuildVersionSetView extends EasCommand {
static description = 'Update version of an app.';
static hidden = true;
static flags = {
platform: Flags.enum({
@@ -49,13 +48,13 @@ export default class BuildVersionSetView extends EasCommand {
const platform = await selectPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureRemoteVersionPolicyAsync(projectDir, easJsonReader);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const profile = await easJsonReader.getBuildProfileAsync(platform, flags.profile ?? undefined);
const exp = getExpoConfig(projectDir, { env: profile.env });
const projectId = await getProjectIdAsync(exp);
const projectFullName = await getProjectFullNameAsync(exp);
await validateAppConfigForRemoteVersionPolicyAsync(exp);
validateAppConfigForRemoteVersionSource(exp, platform);
const applicationIdentifier = await getApplicationIdentifierAsync(
projectDir,
@@ -24,9 +24,9 @@ import { resolveTargetsAsync } from '../../../project/ios/target';
import { BUILD_NUMBER_REQUIREMENTS, isValidBuildNumber } from '../../../project/ios/versions';
import { findProjectRootAsync, getProjectIdAsync } from '../../../project/projectUtils';
import {
ensureRemoteVersionPolicyAsync,
ensureVersionSourceIsRemoteAsync,
getBuildVersionName,
validateAppConfigForRemoteVersionPolicyAsync,
validateAppConfigForRemoteVersionSource,
} from '../../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../../project/workflow';
import { getProfilesAsync } from '../../../utils/profiles';
@@ -41,7 +41,6 @@ interface SyncContext<T extends Platform> {
export default class BuildVersionSyncView extends EasCommand {
public static description = 'Update a version in native code with a value stored on EAS servers';
static hidden = true;
public static flags = {
platform: Flags.enum({
@@ -61,7 +60,7 @@ export default class BuildVersionSyncView extends EasCommand {
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonReader = new EasJsonReader(projectDir);
await ensureRemoteVersionPolicyAsync(projectDir, easJsonReader);
await ensureVersionSourceIsRemoteAsync(projectDir, easJsonReader);
const platforms = toPlatforms(requestedPlatform);
const buildProfiles = await getProfilesAsync({
@@ -73,7 +72,7 @@ export default class BuildVersionSyncView extends EasCommand {
for (const profileInfo of buildProfiles) {
const exp = getExpoConfig(projectDir, { env: profileInfo.profile.env });
const projectId = await getProjectIdAsync(exp);
await validateAppConfigForRemoteVersionPolicyAsync(exp);
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
const applicationIdentifier = await getApplicationIdentifierAsync(
@@ -51,11 +51,11 @@ export default class MetadataPull extends EasCommand {
const relativePath = path.relative(process.cwd(), filePath);
Log.addNewLineIfNone();
Log.log(`🎉 Your store configuration is ready.
Log.log(`🎉 Your store config is ready.
- Update the ${chalk.bold(relativePath)} file to prepare the app information.
- Run ${chalk.bold('eas submit')} or manually upload a new app version to the app stores.
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store configuration.
- Once the app is uploaded, run ${chalk.bold('eas metadata:push')} to sync the store config.
- ${learnMore('https://docs.expo.dev/eas-metadata/introduction/')}`);
} catch (error: any) {
handleMetadataError(error);
+9 -2
View File
@@ -16,7 +16,11 @@ import {
import { getExpoConfig } from '../project/expoConfig';
import { findProjectRootAsync, getProjectIdAsync } from '../project/projectUtils';
import { SubmitArchiveFlags, createSubmissionContextAsync } from '../submit/context';
import { submitAsync, waitToCompleteAsync } from '../submit/submit';
import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
submitAsync,
waitToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { getProfilesAsync } from '../utils/profiles';
@@ -130,7 +134,10 @@ export default class Submit extends EasCommand {
printSubmissionDetailsUrls(submissions);
if (flags.wait) {
await waitToCompleteAsync(submissions, { verbose: flags.verbose });
const completedSubmissions = await waitToCompleteAsync(submissions, {
verbose: flags.verbose,
});
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
}
@@ -242,8 +242,8 @@ async function configureAppJSONForEASUpdateAsync({
}
function isRuntimeEqual(
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' },
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' }
runtimeVersionA: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' },
runtimeVersionB: string | { policy: 'sdkVersion' | 'nativeVersion' | 'appVersion' }
): boolean {
if (typeof runtimeVersionA === 'string' && typeof runtimeVersionB === 'string') {
return runtimeVersionA === runtimeVersionB;
+19 -3
View File
@@ -28,7 +28,7 @@ import {
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log, { link } from '../../log';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { getExpoConfig } from '../../project/expoConfig';
import {
@@ -43,6 +43,7 @@ import {
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
isUploadedAssetCountAboveWarningThreshold,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
@@ -326,6 +327,7 @@ export default class UpdatePublish extends EasCommand {
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let oldMessage: string, oldRuntimeVersion: string;
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
if (republish) {
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
@@ -446,15 +448,17 @@ export default class UpdatePublish extends EasCommand {
try {
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
const { uniqueUploadedAssetCount } = await uploadAssetsAsync(
const uploadResults = await uploadAssetsAsync(
assets,
projectId,
(totalAssets, missingAssets) => {
assetSpinner.text = `Uploading assets. Finished (${
totalAssets - missingAssets
}/${totalAssets})`;
}
);
uploadedAssetCount = uniqueUploadedAssetCount;
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
@@ -604,6 +608,18 @@ export default class UpdatePublish extends EasCommand {
])
);
Log.addNewLineIfNone();
if (
isUploadedAssetCountAboveWarningThreshold(uploadedAssetCount, assetLimitPerUpdateGroup)
) {
Log.warn(
`This update group contains ${uploadedAssetCount} assets and is nearing the server cap of ${assetLimitPerUpdateGroup}.\n` +
`${learnMore('https://docs.expo.dev/eas-update/optimize-assets/', {
learnMoreMessage: 'Consider optimizing your usage of assets',
dim: false,
})}.`
);
Log.addNewLineIfNone();
}
}
}
}
@@ -18,6 +18,7 @@ export const jester2: Actor = {
export const testUsername = jester.username;
export const testSlug = 'testApp';
export const testProjectId = '7ef93448-3bc7-4b57-be32-99326dcf24f0';
export const testBundleIdentifier = 'test.com.app';
export const testPackageName = 'test.com.app';
export const testExperienceName = `@${testUsername}/${testSlug}`;
@@ -35,6 +36,11 @@ export const testAppJson = {
sdkVersion: '38.0.0',
ios: { bundleIdentifier: testBundleIdentifier },
android: { package: testPackageName },
extra: {
eas: {
projectId: testProjectId,
},
},
};
export const testAppJsonWithDifferentOwner = {
+98 -4
View File
@@ -17,13 +17,9 @@ export type Scalars = {
Boolean: boolean;
Int: number;
Float: number;
/** Date custom scalar type */
DateTime: any;
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSON: any;
/** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSONObject: any;
/** The `Upload` scalar type represents a file upload. */
Upload: any;
};
@@ -109,6 +105,8 @@ export type Account = {
createdAt: Scalars['DateTime'];
/** Environment secrets for an account */
environmentSecrets: Array<EnvironmentSecret>;
/** GitHub App installations for an account */
githubAppInstallations: Array<GitHubAppInstallation>;
/** Android credentials for account */
googleServiceAccountKeys: Array<GoogleServiceAccountKey>;
id: Scalars['ID'];
@@ -894,6 +892,7 @@ export type App = Project & {
androidAppCredentials: Array<AndroidAppCredentials>;
/** ios.appStoreUrl field from most recent classic update manifest */
appStoreUrl?: Maybe<Scalars['String']>;
assetLimitPerUpdateGroup: Scalars['Int'];
buildJobs: Array<BuildJob>;
/**
* Coalesced Build (EAS) or BuildJob (Classic) items for this app.
@@ -1751,6 +1750,7 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
reactNativeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
resourceClass?: Maybe<BuildResourceClass>;
runFromCI?: Maybe<Scalars['Boolean']>;
runtimeVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
status: BuildStatus;
@@ -1901,6 +1901,8 @@ export type BuildMetadataInput = {
message?: InputMaybe<Scalars['String']>;
reactNativeVersion?: InputMaybe<Scalars['String']>;
releaseChannel?: InputMaybe<Scalars['String']>;
runFromCI?: InputMaybe<Scalars['Boolean']>;
runWithNoWaitFlag?: InputMaybe<Scalars['Boolean']>;
runtimeVersion?: InputMaybe<Scalars['String']>;
sdkVersion?: InputMaybe<Scalars['String']>;
trackingContext?: InputMaybe<Scalars['JSONObject']>;
@@ -2164,6 +2166,11 @@ export type CreateEnvironmentSecretInput = {
value: Scalars['String'];
};
export type CreateGitHubAppInstallationInput = {
accountId: Scalars['ID'];
installationIdentifier: Scalars['Int'];
};
export type CreateIosSubmissionInput = {
appId: Scalars['ID'];
archiveUrl?: InputMaybe<Scalars['String']>;
@@ -2171,6 +2178,12 @@ export type CreateIosSubmissionInput = {
submittedBuildId?: InputMaybe<Scalars['ID']>;
};
export type CreateServerlessFunctionUploadUrlResult = {
__typename?: 'CreateServerlessFunctionUploadUrlResult';
formDataFields: Scalars['JSONObject'];
url: Scalars['String'];
};
export type CreateSubmissionResult = {
__typename?: 'CreateSubmissionResult';
/** Created submission */
@@ -2289,6 +2302,7 @@ export enum EasServiceMetric {
BandwidthUsage = 'BANDWIDTH_USAGE',
Builds = 'BUILDS',
ManifestRequests = 'MANIFEST_REQUESTS',
UniqueUpdaters = 'UNIQUE_UPDATERS',
UniqueUsers = 'UNIQUE_USERS'
}
@@ -2395,6 +2409,38 @@ export type GetSignedAssetUploadSpecificationsResult = {
specifications: Array<Scalars['String']>;
};
export type GitHubAppInstallation = {
__typename?: 'GitHubAppInstallation';
account: Account;
actor?: Maybe<Actor>;
id: Scalars['ID'];
installationIdentifier: Scalars['Int'];
};
export type GitHubAppInstallationMutation = {
__typename?: 'GitHubAppInstallationMutation';
/** Create a GitHub App installation for an Account */
createGitHubAppInstallationForAccount: GitHubAppInstallation;
/** Delete a GitHub App installation by ID */
deleteGitHubAppInstallation: GitHubAppInstallation;
};
export type GitHubAppInstallationMutationCreateGitHubAppInstallationForAccountArgs = {
githubAppInstallationData: CreateGitHubAppInstallationInput;
};
export type GitHubAppInstallationMutationDeleteGitHubAppInstallationArgs = {
githubAppInstallationId: Scalars['ID'];
};
export type GitHubAppQuery = {
__typename?: 'GitHubAppQuery';
appIdentifier: Scalars['String'];
clientIdentifier: Scalars['String'];
};
export type GoogleServiceAccountKey = {
__typename?: 'GoogleServiceAccountKey';
account: Account;
@@ -3121,6 +3167,8 @@ export type RootMutation = {
emailSubscription: EmailSubscriptionMutation;
/** Mutations that create and delete EnvironmentSecrets */
environmentSecret: EnvironmentSecretMutation;
/** Mutations for GitHub App installations */
githubAppInstallation: GitHubAppInstallationMutation;
/** Mutations that modify a Google Service Account Key */
googleServiceAccountKey: GoogleServiceAccountKeyMutation;
/** Mutations that modify the build credentials for an iOS app */
@@ -3132,6 +3180,7 @@ export type RootMutation = {
me: MeMutation;
/** Mutations that create, update, and delete Robots */
robot: RobotMutation;
serverlessFunction: ServerlessFunctionMutation;
/** Mutations that modify an EAS Submit submission */
submission: SubmissionMutation;
update: UpdateMutation;
@@ -3166,6 +3215,8 @@ export type RootMutationBuildJobArgs = {
export type RootQuery = {
__typename?: 'RootQuery';
/** Top-level query object for querying GitHub App information and resources it has access to. */
GitHubApp: GitHubAppQuery;
/**
* This is a placeholder field
* @deprecated Not used.
@@ -3299,6 +3350,21 @@ export type SecondFactorRegenerateBackupCodesResult = {
plaintextBackupCodes: Array<Scalars['String']>;
};
export type ServerlessFunctionIdentifierInput = {
gitCommitSHA1: Scalars['String'];
};
export type ServerlessFunctionMutation = {
__typename?: 'ServerlessFunctionMutation';
createUploadPresignedUrl: CreateServerlessFunctionUploadUrlResult;
};
export type ServerlessFunctionMutationCreateUploadPresignedUrlArgs = {
appId: Scalars['ID'];
serverlessFunctionIdentifierInput: ServerlessFunctionIdentifierInput;
};
export type Snack = Project & {
__typename?: 'Snack';
/** Description of the Snack */
@@ -3358,6 +3424,16 @@ export enum StandardOffer {
YearlySub = 'YEARLY_SUB'
}
export type StripeCoupon = {
__typename?: 'StripeCoupon';
amountOff?: Maybe<Scalars['String']>;
appliesTo?: Maybe<Scalars['String']>;
id: Scalars['ID'];
name: Scalars['String'];
percentOff?: Maybe<Scalars['Float']>;
valid: Scalars['Boolean'];
};
/** Represents an EAS Submission */
export type Submission = ActivityTimelineProjectActivity & {
__typename?: 'Submission';
@@ -3472,6 +3548,7 @@ export type SubscriptionDetails = {
addons: Array<AddonDetails>;
cancelledAt?: Maybe<Scalars['DateTime']>;
concurrencies?: Maybe<Concurrencies>;
coupon?: Maybe<StripeCoupon>;
endedAt?: Maybe<Scalars['DateTime']>;
futureSubscription?: Maybe<FutureSubscription>;
id: Scalars['ID'];
@@ -3673,6 +3750,7 @@ export enum UsageMetricType {
Bandwidth = 'BANDWIDTH',
Build = 'BUILD',
Request = 'REQUEST',
Update = 'UPDATE',
User = 'USER'
}
@@ -4677,6 +4755,13 @@ export type BuildsByIdQueryVariables = Exact<{
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type GetAllBuildsForAppQueryVariables = Exact<{
appId: Scalars['String'];
offset: Scalars['Int'];
@@ -4724,6 +4809,13 @@ export type GetAssetMetadataQueryVariables = Exact<{
export type GetAssetMetadataQuery = { __typename?: 'RootQuery', asset: { __typename?: 'AssetQuery', metadata: Array<{ __typename?: 'AssetMetadataResult', storageKey: string, status: AssetMetadataStatus }> } };
export type GetAssetLimitPerUpdateGroupForAppQueryVariables = Exact<{
appId: Scalars['String'];
}>;
export type GetAssetLimitPerUpdateGroupForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, assetLimitPerUpdateGroup: number } } };
export type SubmissionsByIdQueryVariables = Exact<{
submissionId: Scalars['ID'];
}>;
@@ -4785,6 +4877,8 @@ export type AppFragment = { __typename?: 'App', id: string, fullName: string, sl
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, createdAt: any };
export type SubmissionFragment = { __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null };
@@ -3,19 +3,20 @@ import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import {
CodeSigningInfoInput,
GetSignedUploadMutation,
GetSignedUploadMutationVariables,
PublishUpdateGroupInput,
SetCodeSigningInfoMutation,
UpdatePublishMutation,
} from '../generated';
export const PublishMutation = {
async getUploadURLsAsync(contentTypes: string[]): Promise<{ specifications: string[] }> {
async getUploadURLsAsync(
contentTypes: string[]
): Promise<GetSignedUploadMutation['asset']['getSignedAssetUploadSpecifications']> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<
{ asset: { getSignedAssetUploadSpecifications: { specifications: string[] } } },
{ contentTypes: string[] }
>(
.mutation<GetSignedUploadMutation, GetSignedUploadMutationVariables>(
gql`
mutation GetSignedUploadMutation($contentTypes: [String!]!) {
asset {
@@ -33,7 +34,6 @@ export const PublishMutation = {
);
return data.asset.getSignedAssetUploadSpecifications;
},
async publishUpdateGroupAsync(
publishUpdateGroupsInput: PublishUpdateGroupInput[]
): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
@@ -6,13 +6,16 @@ import {
AppPlatform,
BuildFragment,
BuildStatus,
BuildWithSubmissionsFragment,
BuildsByIdQuery,
BuildsByIdQueryVariables,
BuildsWithSubmissionsByIdQuery,
BuildsWithSubmissionsByIdQueryVariables,
DistributionType,
GetAllBuildsForAppQuery,
GetAllBuildsForAppQueryVariables,
} from '../generated';
import { BuildFragmentNode } from '../types/Build';
import { BuildFragmentNode, BuildFragmentWithSubmissionsNode } from '../types/Build';
type BuildsQuery = {
offset?: number;
@@ -63,6 +66,35 @@ export const BuildQuery = {
return data.builds.byId;
},
async withSubmissionsByIdAsync(
buildId: string,
{ useCache = true }: { useCache?: boolean } = {}
): Promise<BuildWithSubmissionsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BuildsWithSubmissionsByIdQuery, BuildsWithSubmissionsByIdQueryVariables>(
gql`
query BuildsWithSubmissionsByIdQuery($buildId: ID!) {
builds {
byId(buildId: $buildId) {
id
...BuildWithSubmissionsFragment
}
}
}
${print(BuildFragmentWithSubmissionsNode)}
`,
{ buildId },
{
requestPolicy: useCache ? 'cache-first' : 'network-only',
additionalTypenames: ['Build'],
}
)
.toPromise()
);
return data.builds.byId;
},
async allForAppAsync(
appId: string,
{ limit = 10, offset = 0, filter }: BuildsQuery
@@ -1,7 +1,12 @@
import gql from 'graphql-tag';
import { graphqlClient, withErrorHandlingAsync } from '../client';
import { AssetMetadataResult, GetAssetMetadataQuery } from '../generated';
import {
AssetMetadataResult,
GetAssetLimitPerUpdateGroupForAppQuery,
GetAssetLimitPerUpdateGroupForAppQueryVariables,
GetAssetMetadataQuery,
} from '../generated';
export const PublishQuery = {
async getAssetMetadataAsync(storageKeys: string[]): Promise<AssetMetadataResult[]> {
@@ -30,4 +35,30 @@ export const PublishQuery = {
);
return data.asset.metadata;
},
async getAssetLimitPerUpdateGroupAsync(
appId: string
): Promise<GetAssetLimitPerUpdateGroupForAppQuery['app']['byId']['assetLimitPerUpdateGroup']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<
GetAssetLimitPerUpdateGroupForAppQuery,
GetAssetLimitPerUpdateGroupForAppQueryVariables
>(
gql`
query GetAssetLimitPerUpdateGroupForApp($appId: String!) {
app {
byId(appId: $appId) {
id
assetLimitPerUpdateGroup
}
}
}
`,
{ appId },
{ additionalTypenames: [] } // required arg
)
.toPromise()
);
return data.app.byId.assetLimitPerUpdateGroup;
},
};
@@ -1,5 +1,8 @@
import { print } from 'graphql';
import gql from 'graphql-tag';
import { SubmissionFragmentNode } from './Submission';
export const BuildFragmentNode = gql`
fragment BuildFragment on Build {
id
@@ -49,3 +52,17 @@ export const BuildFragmentNode = gql`
updatedAt
}
`;
export const BuildFragmentWithSubmissionsNode = gql`
${print(SubmissionFragmentNode)}
${print(BuildFragmentNode)}
fragment BuildWithSubmissionsFragment on Build {
id
...BuildFragment
submissions {
id
...SubmissionFragment
}
}
`;
@@ -0,0 +1,128 @@
import path from 'path';
import { getStaticConfigFilePath, loadConfigAsync } from '../config';
import { MetadataValidationError } from '../errors';
describe(getStaticConfigFilePath, () => {
const projectDir = '/app';
it(`returns same file for store.config.json`, () => {
const metadataPath = 'store.config.json';
const expectedFile = path.join(projectDir, metadataPath);
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
it(`returns store.config.json for store.config.js`, () => {
const metadataPath = 'store.config.js';
const expectedFile = path.join(projectDir, 'store.config.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
it(`returns store.staging.json file for store.staging.js`, () => {
const metadataPath = 'store.staging.js';
const expectedFile = path.join(projectDir, 'store.staging.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
// This shouldn't be used IRL, but it tests if this function is working properly
it(`returns custom-name.json file for custom-name.js`, () => {
const metadataPath = 'custom-name.js';
const expectedFile = path.join(projectDir, 'custom-name.json');
expect(getStaticConfigFilePath({ projectDir, metadataPath })).toBe(expectedFile);
});
});
describe(loadConfigAsync, () => {
const projectDir = path.resolve(__dirname, 'fixtures');
it(`throws when file doesn't exist`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'doesnt-exist.json' })
).rejects.toThrow('file not found');
});
it(`throws when validation errors are found without skipping validation`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json' })
).rejects.toThrow('errors found');
});
it(`returns config from "store.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.config.json' })
).resolves.toHaveProperty('apple.copyright', 'ACME');
});
it(`returns config from "store.config.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.config.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.function.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.function.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns config from "store.async.js"`, async () => {
const year = new Date().getFullYear();
await expect(
loadConfigAsync({ projectDir, metadataPath: 'store.async.js' })
).resolves.toHaveProperty('apple.copyright', `${year} ACME`);
});
it(`returns invalid config from "invalid.config.json`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.json', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.config.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.config.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.function.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.function.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`returns invalid config from "invalid.async.js`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid.async.js', skipValidation: true })
).resolves.toMatchObject({ configVersion: -1 });
});
it(`throws invalid config type from "invalid-type.config.json"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.json' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.config.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.config.js' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.function.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.function.js' })
).rejects.toThrow(MetadataValidationError);
});
it(`throws invalid config type from "invalid-type.async.js"`, async () => {
await expect(
loadConfigAsync({ projectDir, metadataPath: 'invalid-type.async.js' })
).rejects.toThrow(MetadataValidationError);
});
});
@@ -0,0 +1,3 @@
module.exports = async function () {
return await Promise.resolve(null);
};
@@ -0,0 +1 @@
module.exports = 1337;
@@ -0,0 +1,5 @@
[
{
"configVersion": -1
}
]
@@ -0,0 +1,3 @@
module.exports = function () {
return true;
};
@@ -0,0 +1,3 @@
module.exports = async function () {
return await Promise.resolve({ configVersion: -1 });
};
@@ -0,0 +1,3 @@
module.exports = {
configVersion: -1,
};
@@ -0,0 +1,3 @@
{
"configVersion": -1
}
@@ -0,0 +1,3 @@
module.exports = function () {
return { configVersion: -1 };
};
@@ -0,0 +1,5 @@
module.exports = async function () {
const config = require('./store.config.json');
config.apple.copyright = `${new Date().getFullYear()} ACME`;
return await Promise.resolve(config);
};
@@ -0,0 +1,5 @@
const config = require('./store.config.json');
config.apple.copyright = `${new Date().getFullYear()} ACME`;
module.exports = config;
@@ -0,0 +1,22 @@
{
"configVersion": 0,
"apple": {
"copyright": "ACME",
"info": {
"en-US": {
"title": "App title",
"subtitle": "Subtitle for your app",
"description": "A longer description of what your app does",
"keywords": [
"keyword"
],
"releaseNotes": "Bug fixes and improved stability",
"promoText": "Short tagline for your app",
"marketingUrl": "https://example.com/en/marketing",
"supportUrl": "https://example.com/en/support",
"privacyPolicyUrl": "https://example.com/en/privacy",
"privacyChoicesUrl": "https://example.com/en/choices"
}
}
}
}
@@ -0,0 +1,5 @@
module.exports = function () {
const config = require('./store.config.json');
config.apple.copyright = `${new Date().getFullYear()} ACME`;
return config;
};
@@ -0,0 +1,10 @@
import { AppStoreVersionPhasedRelease, PhasedReleaseState } from '@expo/apple-utils';
import { AttributesOf } from '../../../../utils/asc';
export const phasedRelease: AttributesOf<AppStoreVersionPhasedRelease> = {
phasedReleaseState: PhasedReleaseState.COMPLETE,
currentDayNumber: 7,
startDate: null,
totalPauseDuration: null,
};
@@ -1,4 +1,9 @@
import { AppCategoryId, AppSubcategoryId } from '@expo/apple-utils';
import {
AppCategoryId,
AppSubcategoryId,
PhasedReleaseState,
ReleaseType,
} from '@expo/apple-utils';
import { AppleConfigReader } from '../reader';
import { leastRestrictiveAdvisory, mostRestrictiveAdvisory } from './fixtures/ageRatingDeclaration';
@@ -83,3 +88,91 @@ describe('getAgeRating', () => {
expect(reader.getAgeRating()).toMatchObject(mostRestrictiveAdvisory);
});
});
describe('getVersionReleaseType', () => {
it('ignores automatic release when not set', () => {
const reader = new AppleConfigReader({ release: undefined });
expect(reader.getVersionReleaseType()).toBeNull();
});
it('ignores automatic release when automaticRelease not set', () => {
const reader = new AppleConfigReader({ release: {} });
expect(reader.getVersionReleaseType()).toBeNull();
});
it('returns scheduled release date with iso string', () => {
const reader = new AppleConfigReader({
release: { automaticRelease: '2020-06-17T12:00:00-00:00' },
});
expect(reader.getVersionReleaseType()).toMatchObject({
releaseType: ReleaseType.SCHEDULED,
earliestReleaseDate: '2020-06-17T12:00:00.000Z',
});
});
it('returns scheduled release with unparsable date string', () => {
const reader = new AppleConfigReader({ release: { automaticRelease: '2020-06-17-12:00:00' } });
expect(reader.getVersionReleaseType()).toMatchObject({
releaseType: ReleaseType.SCHEDULED,
earliestReleaseDate: '2020-06-17-12:00:00',
});
});
it('returns automatic release', () => {
const reader = new AppleConfigReader({ release: { automaticRelease: true } });
expect(reader.getVersionReleaseType()).toMatchObject({
releaseType: ReleaseType.AFTER_APPROVAL,
earliestReleaseDate: null,
});
});
it('returns manual release', () => {
const reader = new AppleConfigReader({ release: { automaticRelease: false } });
expect(reader.getVersionReleaseType()).toMatchObject({
releaseType: ReleaseType.MANUAL,
earliestReleaseDate: null,
});
});
});
describe('getVersionReleasePhased', () => {
it('ignores phased release when not set', () => {
const reader = new AppleConfigReader({ release: undefined });
expect(reader.getVersionReleasePhased()).toBeNull();
});
it('ignores phased release when phasedRelease not set', () => {
const reader = new AppleConfigReader({ release: {} });
expect(reader.getVersionReleasePhased()).toBeNull();
});
it('returns phased release when enabled', () => {
const reader = new AppleConfigReader({ release: { phasedRelease: true } });
expect(reader.getVersionReleasePhased()).toMatchObject({
phasedReleaseState: PhasedReleaseState.ACTIVE,
});
});
it('ignores phased release when disabled', () => {
const reader = new AppleConfigReader({ release: { phasedRelease: false } });
expect(reader.getVersionReleasePhased()).toBeNull();
});
});
describe('getVersion', () => {
it('ignores version when not set', () => {
const reader = new AppleConfigReader({});
expect(reader.getVersion()).toBeNull();
});
it('returns version and copyright when set', () => {
const reader = new AppleConfigReader({
version: '2.0',
copyright: '2022 - ACME',
});
expect(reader.getVersion()).toMatchObject({
versionString: '2.0',
copyright: '2022 - ACME',
});
});
});
@@ -12,6 +12,7 @@ import { dutchInfo, englishInfo } from './fixtures/appInfoLocalization';
import { nameAndDemoReviewDetails, nameOnlyReviewDetails } from './fixtures/appStoreReviewDetail';
import { automaticRelease, manualRelease, scheduledRelease } from './fixtures/appStoreVersion';
import { dutchVersion, englishVersion } from './fixtures/appStoreVersionLocalization';
import { phasedRelease } from './fixtures/appStoreVersionPhasedRelease';
describe('toSchema', () => {
it('returns object with apple schema', () => {
@@ -182,41 +183,72 @@ describe('setCategories', () => {
});
describe('setVersion', () => {
it('modifies the copyright', () => {
it('modifies the copyright and version string', () => {
const writer = new AppleConfigWriter();
writer.setVersion(manualRelease);
expect(writer.schema.version).toBe(manualRelease.versionString);
expect(writer.schema.copyright).toBe(manualRelease.copyright);
});
});
describe('setVersionRelease', () => {
describe('setVersionReleaseType', () => {
it('modifies scheduled release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(scheduledRelease);
writer.setVersionReleaseType(scheduledRelease);
expect(writer.schema.release).toMatchObject({
autoReleaseDate: scheduledRelease.earliestReleaseDate,
automaticRelease: scheduledRelease.earliestReleaseDate,
});
});
it('modifies automatic release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(automaticRelease);
writer.setVersionReleaseType(automaticRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: true,
});
});
it('does not modify manual release', () => {
it('modifies manual release', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(manualRelease);
expect(writer.schema.release).toBeUndefined();
writer.setVersionReleaseType(manualRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: false,
});
});
it('overwrites all release fields', () => {
it('does not overwrite phasedRelease', () => {
const writer = new AppleConfigWriter();
writer.setVersionRelease(scheduledRelease);
writer.setVersionRelease(automaticRelease);
expect(writer.schema.release).not.toHaveProperty('autoReleaseDate');
writer.setVersionReleasePhased(phasedRelease);
writer.setVersionReleaseType(manualRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: false,
phasedRelease: true,
});
});
});
describe('setVersionReleasePhased', () => {
it('modifies enabled phased release', () => {
const writer = new AppleConfigWriter();
writer.setVersionReleasePhased(phasedRelease);
expect(writer.schema.release).toHaveProperty('phasedRelease', true);
});
it('deletes phased release when undefined', () => {
const writer = new AppleConfigWriter();
writer.setVersionReleasePhased(phasedRelease);
writer.setVersionReleasePhased(undefined);
expect(writer.schema.release).not.toHaveProperty('phasedRelease');
});
it('does not overwrite automaticRelease', () => {
const writer = new AppleConfigWriter();
writer.setVersionReleaseType(automaticRelease);
writer.setVersionReleasePhased(phasedRelease);
expect(writer.schema.release).toMatchObject({
automaticRelease: true,
phasedRelease: true,
});
});
});
@@ -4,7 +4,9 @@ import {
AppStoreReviewDetail,
AppStoreVersion,
AppStoreVersionLocalization,
AppStoreVersionPhasedRelease,
CategoryIds,
PhasedReleaseState,
Rating,
ReleaseType,
} from '@expo/apple-utils';
@@ -111,19 +113,26 @@ export class AppleConfigReader {
public getVersion(): Partial<
Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
return this.schema.copyright ? { copyright: this.schema.copyright } : null;
const attributes: Pick<AttributesOf<AppStoreVersion>, 'versionString' | 'copyright'> = {
versionString: this.schema.version ?? '',
copyright: this.schema.copyright ?? null,
};
const hasValues = Object.values(attributes).some(Boolean);
return hasValues ? attributes : null;
}
public getVersionRelease(): Partial<
public getVersionReleaseType(): Partial<
Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
> | null {
const { release } = this.schema;
if (release?.autoReleaseDate) {
if (typeof release?.automaticRelease === 'string') {
return {
releaseType: ReleaseType.SCHEDULED,
// Convert time format to 2020-06-17T12:00:00-07:00
earliestReleaseDate: removeDatePrecision(release.autoReleaseDate)?.toISOString() ?? null,
// Convert time format to 2020-06-17T12:00:00-07:00, if that fails, try the date anyways.
earliestReleaseDate:
removeDatePrecision(release.automaticRelease)?.toISOString() ?? release.automaticRelease,
};
}
@@ -144,6 +153,21 @@ export class AppleConfigReader {
return null;
}
public getVersionReleasePhased(): Pick<
AttributesOf<AppStoreVersionPhasedRelease>,
'phasedReleaseState'
> | null {
if (this.schema.release?.phasedRelease === true) {
return {
phasedReleaseState: PhasedReleaseState.ACTIVE,
};
}
// When phased release is turned off, we need to delete the phased release request.
// There is no concept (yet) of pausing the phased release through EAS metadata.
return null;
}
public getVersionLocale(
locale: string,
context: { versionIsFirst: boolean }
@@ -5,6 +5,7 @@ import {
AppStoreReviewDetail,
AppStoreVersion,
AppStoreVersionLocalization,
AppStoreVersionPhasedRelease,
Rating,
ReleaseType,
} from '@expo/apple-utils';
@@ -108,28 +109,43 @@ export class AppleConfigWriter {
public setVersion(
attributes: Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
this.schema.version = optional(attributes.versionString);
this.schema.copyright = optional(attributes.copyright);
}
public setVersionRelease(
public setVersionReleaseType(
attributes: Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
): void {
if (attributes.releaseType === ReleaseType.SCHEDULED) {
if (attributes.releaseType === ReleaseType.SCHEDULED && attributes.earliestReleaseDate) {
this.schema.release = {
autoReleaseDate: optional(attributes.earliestReleaseDate),
...this.schema.release,
automaticRelease: attributes.earliestReleaseDate,
};
}
if (attributes.releaseType === ReleaseType.AFTER_APPROVAL) {
this.schema.release = {
...this.schema.release,
automaticRelease: true,
};
}
if (attributes.releaseType === ReleaseType.MANUAL) {
// ReleaseType.MANUAL is the default behavior, so we don't need to configure it.
// Setting `"automaticRelease": false` is a bit confusing for people who don't know what automaticRelease does.
this.schema.release = undefined;
this.schema.release = {
...this.schema.release,
automaticRelease: false,
};
}
}
public setVersionReleasePhased(attributes?: AttributesOf<AppStoreVersionPhasedRelease>): void {
if (!attributes) {
delete this.schema.release?.phasedRelease;
} else {
this.schema.release = {
...this.schema.release,
phasedRelease: true,
};
}
}
@@ -3,6 +3,8 @@ import {
AppStoreState,
AppStoreVersion,
AppStoreVersionLocalization,
AppStoreVersionPhasedRelease,
PhasedReleaseState,
} from '@expo/apple-utils';
import nock from 'nock';
@@ -27,6 +29,12 @@ describe(AppVersionTask, () => {
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getPhasedReleaseAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/appStoreVersionPhasedRelease`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionPhasedRelease-200-empty.json')
)
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
@@ -60,6 +68,12 @@ describe(AppVersionTask, () => {
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query(true)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getPhasedReleaseAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/appStoreVersionPhasedRelease`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionPhasedRelease-200-empty.json')
)
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
@@ -89,6 +103,12 @@ describe(AppVersionTask, () => {
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
// Respond to version.getPhasedReleaseAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/appStoreVersionPhasedRelease`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionPhasedRelease-200-empty.json')
)
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
@@ -118,6 +138,12 @@ describe(AppVersionTask, () => {
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200.json'))
// Respond to version.getPhasedReleaseAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/appStoreVersionPhasedRelease`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionPhasedRelease-200-empty.json')
)
// Respond to version.getLocalizationsAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
.reply(
@@ -134,6 +160,97 @@ describe(AppVersionTask, () => {
expect(context.versionIsFirst).toBeFalsy();
expect(scope.isDone()).toBeTruthy();
});
it('fetches the existing version from options', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getAppStoreVersionsAsync in findEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query(true)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200.json'))
// Respond to version.getLocalizationsAsync (for version 2.0)
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_2/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
)
// Respond to version.getPhasedReleaseAsync
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_2/appStoreVersionPhasedRelease`)
.reply(200, {
data: {
id: 'APP_STORE_VERSION_PHASED_RELEASE_1',
type: AppStoreVersionPhasedRelease.type,
attributes: {
phasedReleaseState: PhasedReleaseState.INACTIVE,
currentDayNumber: null,
startDate: null,
totalPauseDuration: null,
},
},
});
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask({ version: '2.0' }).prepareAsync({ context });
expect(context.version).toBeInstanceOf(AppStoreVersion);
expect(context.version?.attributes).toHaveProperty('versionString', '2.0');
expect(scope.isDone()).toBeTruthy();
});
it('creates the non-existing version from options', async () => {
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to app.getAppStoreVersionsAsync in findEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query(true)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200.json'))
// Respond to app.getAppStoreVersionsAsync in createOrUpdateEditAppStoreVersionAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query(true)
.reply(200, require('./fixtures/apps/get-appStoreVersions-200-empty.json'))
// Respond to app.createVersionAsync in createOrUpdateEditAppStoreVersionAsync
.post(`/v1/${AppStoreVersion.type}`)
.reply(201, require('./fixtures/appStoreVersions/post-201.json'))
// Respond to app.getAppStoreVersionsAsync
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
.query((params: any) => params['filter[appStoreState]'] !== AppStoreState.READY_FOR_SALE)
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-multiple-200.json'))
// Respond to version.getLocalizationsAsync (for version 3.0)
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_3/${AppStoreVersionLocalization.type}`)
.reply(
200,
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
)
// Respond to version.getPhasedReleaseAsync (for version 3.0)
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_3/appStoreVersionPhasedRelease`)
.reply(200, {
data: {
id: 'APP_STORE_VERSION_PHASED_RELEASE_1',
type: AppStoreVersionPhasedRelease.type,
attributes: {
phasedReleaseState: PhasedReleaseState.INACTIVE,
currentDayNumber: null,
startDate: null,
totalPauseDuration: null,
},
},
});
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
};
await new AppVersionTask({ version: '3.0' }).prepareAsync({ context });
expect(context.version).toBeInstanceOf(AppStoreVersion);
expect(context.version?.attributes).toHaveProperty('versionString', '3.0');
expect(scope.isDone()).toBeTruthy();
});
});
describe('downloadAsync', () => {
@@ -158,7 +275,7 @@ describe(AppVersionTask, () => {
expect(writer.setVersion).toBeCalledWith(version.attributes);
});
it('sets version release when loaded', async () => {
it('sets version release type when loaded', async () => {
const writer = new AppleConfigWriter();
const version = new AppStoreVersion(requestContext, 'stub-id', {} as any);
@@ -167,7 +284,24 @@ describe(AppVersionTask, () => {
context: { version, versionLocales: [] } as any,
});
expect(writer.setVersionRelease).toBeCalledWith(version.attributes);
expect(writer.setVersionReleaseType).toBeCalledWith(version.attributes);
});
it('sets version phased release when loaded', async () => {
const writer = new AppleConfigWriter();
const version = new AppStoreVersion(requestContext, 'stub-id', {} as any);
const versionPhasedRelease = new AppStoreVersionPhasedRelease(
requestContext,
'stub-id',
{} as any
);
await new AppVersionTask().downloadAsync({
config: writer,
context: { version, versionPhasedRelease, versionLocales: [] } as any,
});
expect(writer.setVersionReleasePhased).toBeCalledWith(versionPhasedRelease.attributes);
});
it('skips when no locales are loaded', async () => {
@@ -220,7 +354,7 @@ describe(AppVersionTask, () => {
release: { automaticRelease: true },
});
const attributes = { ...config.getVersion(), ...config.getVersionRelease() };
const attributes = { ...config.getVersion(), ...config.getVersionReleaseType() };
const scope = nock('https://api.appstoreconnect.apple.com')
// Respond to version.updateAsync
.patch(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1`)
@@ -242,5 +376,81 @@ describe(AppVersionTask, () => {
expect(context.version?.attributes).toMatchObject(attributes);
expect(scope.isDone()).toBeTruthy();
});
it('creates phased release when enabled in config', async () => {
const config = new AppleConfigReader({ release: { phasedRelease: true } });
const scope = nock('https://api.appstoreconnect.apple.com')
.post(`/v1/${AppStoreVersionPhasedRelease.type}`)
.reply(201, {
data: {
id: 'APP_STORE_VERSION_PHASED_RELEASE_1',
type: AppStoreVersionPhasedRelease.type,
attributes: {
phasedReleaseState: PhasedReleaseState.ACTIVE,
currentDayNumber: null,
startDate: null,
totalPauseDuration: null,
},
},
});
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
versionPhasedRelease: null, // Not enabled yet
};
await new AppVersionTask().uploadAsync({ context: context as AppleData, config });
expect(context.versionPhasedRelease).toBeInstanceOf(AppStoreVersionPhasedRelease);
expect(scope.isDone()).toBeTruthy();
});
it('deletes active phased release when disabled in config', async () => {
const config = new AppleConfigReader({ release: { phasedRelease: false } });
const scope = nock('https://api.appstoreconnect.apple.com')
.delete(`/v1/${AppStoreVersionPhasedRelease.type}/APP_STORE_VERSION_PHASED_RELEASE_1`)
.reply(204);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
// Enabled, and not completed yet
versionPhasedRelease: new AppStoreVersionPhasedRelease(
requestContext,
'APP_STORE_VERSION_PHASED_RELEASE_1',
{ phasedReleaseState: PhasedReleaseState.ACTIVE } as any
),
};
await new AppVersionTask().uploadAsync({ context: context as AppleData, config });
expect(context.versionPhasedRelease).toBeNull();
expect(scope.isDone()).toBeTruthy();
});
it('does not delete completed phased release when disabled in config', async () => {
const config = new AppleConfigReader({ release: { phasedRelease: false } });
const scope = nock('https://api.appstoreconnect.apple.com')
.delete(`/v1/${AppStoreVersionPhasedRelease.type}/APP_STORE_VERSION_PHASED_RELEASE_1`)
.reply(204);
const context: PartialAppleData = {
app: new App(requestContext, 'stub-id', {} as any),
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
// Enabled, and not completed yet
versionPhasedRelease: new AppStoreVersionPhasedRelease(
requestContext,
'APP_STORE_VERSION_PHASED_RELEASE_1',
{ phasedReleaseState: PhasedReleaseState.COMPLETE } as any
),
};
await new AppVersionTask().uploadAsync({ context: context as AppleData, config });
expect(context.versionPhasedRelease).toBe(context.versionPhasedRelease);
expect(scope.isDone()).toBeFalsy();
nock.cleanAll();
});
});
});
@@ -0,0 +1,6 @@
{
"data": null,
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_PHASED_RELEASE_1/appStoreVersionPhasedRelease"
}
}
@@ -0,0 +1,116 @@
{
"data": [
{
"type": "appStoreVersions",
"id": "APP_STORE_VERSION_3",
"attributes": {
"platform": "IOS",
"versionString": "3.0",
"appStoreState": "PREPARE_FOR_SUBMISSION",
"storeIcon": null,
"watchStoreIcon": null,
"copyright": "2022 name",
"releaseType": "SCHEDULED",
"earliestReleaseDate": "2022-03-14T17:00:00-07:00",
"usesIdfa": null,
"isWatchOnly": false,
"downloadable": true,
"createdDate": "2022-02-21T09:26:24-08:00",
"promotedDate": null
},
"relationships": {
"ageRatingDeclaration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/ageRatingDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/ageRatingDeclaration"
}
},
"appStoreVersionLocalizations": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreVersionLocalizations",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreVersionLocalizations"
}
},
"build": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/build",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/build"
}
},
"appStoreVersionPhasedRelease": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreVersionPhasedRelease",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreVersionPhasedRelease"
}
},
"gameCenterConfiguration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/gameCenterConfiguration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/gameCenterConfiguration"
}
},
"routingAppCoverage": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/routingAppCoverage",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/routingAppCoverage"
}
},
"appStoreReviewDetail": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreReviewDetail",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreReviewDetail"
}
},
"appStoreVersionSubmission": {
"data": null,
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreVersionSubmission",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreVersionSubmission"
}
},
"resetRatingsRequest": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/resetRatingsRequest",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/resetRatingsRequest"
}
},
"idfaDeclaration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/idfaDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/idfaDeclaration"
}
},
"appClipDefaultExperience": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appClipDefaultExperience",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appClipDefaultExperience"
}
},
"appStoreVersionStateChanges": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreVersionStateChanges",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreVersionStateChanges"
}
},
"appStoreVersionExperiments": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/relationships/appStoreVersionExperiments",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3/appStoreVersionExperiments"
}
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_3"
}
}
],
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/apps/1611050397/appStoreVersions?include=appStoreVersionSubmission&filter%5Bplatform%5D=IOS&filter%5BappStoreState%5D=DEVELOPER_REJECTED%2CINVALID_BINARY%2CMETADATA_REJECTED%2CPREPARE_FOR_SUBMISSION%2CREJECTED%2CWAITING_FOR_REVIEW"
},
"meta": {
"paging": {
"total": 1,
"limit": 50
}
}
}
@@ -0,0 +1,12 @@
{
"data": null,
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/apps/1611050397/appStoreVersions?include=appStoreVersionSubmission&filter%5Bplatform%5D=IOS&filter%5BappStoreState%5D=DEVELOPER_REJECTED%2CINVALID_BINARY%2CMETADATA_REJECTED%2CPREPARE_FOR_SUBMISSION%2CREJECTED%2CWAITING_FOR_REVIEW"
},
"meta": {
"paging": {
"total": 0,
"limit": 50
}
}
}
@@ -105,10 +105,10 @@
},
{
"type": "appStoreVersions",
"id": "APP-STORE-VERSION-2",
"id": "APP_STORE_VERSION_2",
"attributes": {
"platform": "IOS",
"versionString": "1.1",
"versionString": "2.0",
"appStoreState": "PREPARE_FOR_SUBMISSION",
"storeIcon": null,
"watchStoreIcon": null,
@@ -124,86 +124,86 @@
"relationships": {
"ageRatingDeclaration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/ageRatingDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/ageRatingDeclaration"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/ageRatingDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/ageRatingDeclaration"
}
},
"appStoreVersionLocalizations": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreVersionLocalizations",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreVersionLocalizations"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreVersionLocalizations",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreVersionLocalizations"
}
},
"build": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/build",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/build"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/build",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/build"
}
},
"appStoreVersionPhasedRelease": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreVersionPhasedRelease",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreVersionPhasedRelease"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreVersionPhasedRelease",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreVersionPhasedRelease"
}
},
"gameCenterConfiguration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/gameCenterConfiguration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/gameCenterConfiguration"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/gameCenterConfiguration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/gameCenterConfiguration"
}
},
"routingAppCoverage": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/routingAppCoverage",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/routingAppCoverage"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/routingAppCoverage",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/routingAppCoverage"
}
},
"appStoreReviewDetail": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreReviewDetail",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreReviewDetail"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreReviewDetail",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreReviewDetail"
}
},
"appStoreVersionSubmission": {
"data": null,
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreVersionSubmission",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreVersionSubmission"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreVersionSubmission",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreVersionSubmission"
}
},
"resetRatingsRequest": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/resetRatingsRequest",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/resetRatingsRequest"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/resetRatingsRequest",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/resetRatingsRequest"
}
},
"idfaDeclaration": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/idfaDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/idfaDeclaration"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/idfaDeclaration",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/idfaDeclaration"
}
},
"appClipDefaultExperience": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appClipDefaultExperience",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appClipDefaultExperience"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appClipDefaultExperience",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appClipDefaultExperience"
}
},
"appStoreVersionStateChanges": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreVersionStateChanges",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreVersionStateChanges"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreVersionStateChanges",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreVersionStateChanges"
}
},
"appStoreVersionExperiments": {
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/relationships/appStoreVersionExperiments",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2/appStoreVersionExperiments"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/relationships/appStoreVersionExperiments",
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2/appStoreVersionExperiments"
}
}
},
"links": {
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2"
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_2"
}
}
],
@@ -1,4 +1,12 @@
import { App, AppStoreVersion, AppStoreVersionLocalization, Platform } from '@expo/apple-utils';
import {
App,
AppStoreState,
AppStoreVersion,
AppStoreVersionLocalization,
AppStoreVersionPhasedRelease,
PhasedReleaseState,
Platform,
} from '@expo/apple-utils';
import assert from 'assert';
import chalk from 'chalk';
@@ -12,6 +20,8 @@ export type AppVersionOptions = {
editLive: boolean;
/** The platform to use (defaults to IOS) */
platform: Platform;
/** A version to create or select, if defined in the store configuration */
version: string | null;
};
export type AppVersionData = {
@@ -23,6 +33,8 @@ export type AppVersionData = {
versionIsFirst: boolean;
/** All version locales that should be, or are enabled */
versionLocales: AppStoreVersionLocalization[];
/** The (existing) phased release configuration, when set */
versionPhasedRelease: AppStoreVersionPhasedRelease | null;
};
export class AppVersionTask extends AppleTask {
@@ -33,6 +45,7 @@ export class AppVersionTask extends AppleTask {
this.options = {
platform: options.platform ?? Platform.IOS,
editLive: options.editLive ?? false,
version: options.version ?? null,
};
}
@@ -50,13 +63,15 @@ export class AppVersionTask extends AppleTask {
context.versionIsFirst = versionIsFirst;
context.versionIsLive = versionIsLive;
context.versionLocales = await version.getLocalizationsAsync();
context.versionPhasedRelease = await version.getPhasedReleaseAsync();
}
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
assert(context.version, `App version not initialized, can't download version`);
config.setVersion(context.version.attributes);
config.setVersionRelease(context.version.attributes);
config.setVersionReleaseType(context.version.attributes);
config.setVersionReleasePhased(context.versionPhasedRelease?.attributes);
for (const locale of context.versionLocales) {
config.setVersionLocale(locale.attributes);
@@ -64,14 +79,31 @@ export class AppVersionTask extends AppleTask {
}
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
assert(context.version, `App version not initialized, can't update version`);
const version = config.getVersion();
const release = config.getVersionRelease();
if (!context.version && version?.versionString) {
context.version = await logAsync(
() => {
return context.app.createVersionAsync({
versionString: version.versionString!,
platform: this.options.platform,
});
},
{
pending: `Creating new version ${chalk.bold(version.versionString)}...`,
success: `Created new version ${chalk.bold(version.versionString)}`,
failure: `Failed creating new version ${chalk.bold(version.versionString)}`,
}
);
}
assert(context.version, `App version not initialized, can't update version`);
const { versionString } = context.version.attributes;
const release = config.getVersionReleaseType();
if (!version && !release) {
Log.log(chalk`{dim - Skipped version and release update, not configured}`);
} else {
const { versionString } = context.version.attributes;
const description = [version && 'version', release && 'release']
.filter(Boolean)
.join(' and ');
@@ -86,6 +118,27 @@ export class AppVersionTask extends AppleTask {
);
}
const phasedRelease = config.getVersionReleasePhased();
if (!phasedRelease && shouldDeletePhasedRelease(context.versionPhasedRelease)) {
// if phased release was enabled, but now disabled, we need to remove it
await logAsync(() => context.versionPhasedRelease!.deleteAsync(), {
pending: `Disabling phased release for ${chalk.bold(versionString)}...`,
success: `Disabled phased release for ${chalk.bold(versionString)}`,
failure: `Failed disabling phased release for ${chalk.bold(versionString)}`,
});
context.versionPhasedRelease = null;
} else if (phasedRelease && !context.versionPhasedRelease) {
// if phased release was not yet set, but now enabled, we need to create it
context.versionPhasedRelease = await logAsync(
() => context.version.createPhasedReleaseAsync({ state: phasedRelease.phasedReleaseState }),
{
pending: `Enabling phased release for ${chalk.bold(versionString)}...`,
success: `Enabled phased release for ${chalk.bold(versionString)}`,
failure: `Failed enabling phased release for ${chalk.bold(versionString)}`,
}
);
}
const locales = config.getLocales();
if (locales.length <= 0) {
Log.log(chalk`{dim - Skipped localized version update, no locales configured}`);
@@ -123,12 +176,12 @@ export class AppVersionTask extends AppleTask {
}
/**
* Resolve the AppStoreVersion instance, either from live or editable version.
* Resolve the AppStoreVersion instance, either from the store config, live, or editable version.
* This also checks if this is the first version, which disallow release notes.
*/
async function resolveVersionAsync(
app: App,
{ editLive, platform }: AppVersionOptions
{ editLive, platform, version: versionString }: AppVersionOptions
): Promise<{
version: AppStoreVersion | null;
versionIsLive: boolean;
@@ -137,7 +190,19 @@ async function resolveVersionAsync(
let version: AppStoreVersion | null = null;
let versionIsLive = false;
if (editLive) {
if (versionString) {
version = await findEditAppStoreVersionAsync(app, { platform, version: versionString });
if (!version) {
version = await createOrUpdateEditAppStoreVersionAsync(app, {
platform,
version: versionString,
});
}
versionIsLive = version?.attributes.appStoreState === AppStoreState.READY_FOR_SALE;
}
if (!version && editLive) {
version = await app.getLiveAppStoreVersionAsync({ platform });
versionIsLive = !!version;
}
@@ -156,3 +221,76 @@ async function resolveVersionAsync(
versionIsFirst: versions.length === 1,
};
}
/**
* Determine if we can, and should, delete the phased release instance.
* This returns true if the instance exist, and has one of the states below:
* - PhasedReleaseState.INACTIVE
* - PhasedReleaseState.ACTIVE
* - PhasedReleaseState.PAUSED
*/
function shouldDeletePhasedRelease(phasedRelease: AppStoreVersionPhasedRelease | null): boolean {
if (
!phasedRelease ||
phasedRelease.attributes.phasedReleaseState === PhasedReleaseState.COMPLETE
) {
return false;
}
return true;
}
/*
* Search for editable app store versions that matches the `versionString` option.
* When nothing is found, it will return `null`, and a new version should be created.
*/
async function findEditAppStoreVersionAsync(
app: App,
options: { version: string; platform: Platform }
): Promise<AppStoreVersion | null> {
if (options.version) {
const versions = await app.getAppStoreVersionsAsync({
query: {
limit: 200,
filter: {
platform: options.platform,
appStoreState: [
AppStoreState.PREPARE_FOR_SUBMISSION,
AppStoreState.DEVELOPER_REJECTED,
AppStoreState.REJECTED,
AppStoreState.METADATA_REJECTED,
AppStoreState.WAITING_FOR_REVIEW,
AppStoreState.INVALID_BINARY,
].join(','),
},
},
});
const version = versions.find(model => model.attributes.versionString === options.version);
if (version) {
return version;
}
}
return null;
}
/**
* Check if we can reuse an existing editable app version that has not been published yet.
* If not, it creates a new version based on the version string.
*/
async function createOrUpdateEditAppStoreVersionAsync(
app: App,
options: { version: string; platform: Platform }
): Promise<AppStoreVersion> {
const version = await app.getEditAppStoreVersionAsync({ platform: options.platform });
if (version) {
return await version.updateAsync({ versionString: options.version });
}
return await app.createVersionAsync({
versionString: options.version,
platform: options.platform,
});
}
@@ -3,11 +3,23 @@ import { AppleTask } from '../task';
import { AgeRatingTask } from './age-rating';
import { AppInfoTask } from './app-info';
import { AppReviewDetailTask } from './app-review-detail';
import { AppVersionTask } from './app-version';
import { AppVersionOptions, AppVersionTask } from './app-version';
type AppleTaskOptions = {
version?: AppVersionOptions['version'];
};
/**
* List of all eligible tasks to sync local store configuration to the App store.
*/
export function createAppleTasks(_ctx: MetadataContext): AppleTask[] {
return [new AppVersionTask(), new AppInfoTask(), new AgeRatingTask(), new AppReviewDetailTask()];
export function createAppleTasks(
_ctx: MetadataContext,
{ version }: AppleTaskOptions = {}
): AppleTask[] {
return [
new AppVersionTask({ version }),
new AppInfoTask(),
new AgeRatingTask(),
new AppReviewDetailTask(),
];
}
+3 -7
View File
@@ -3,6 +3,7 @@ import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
export type AppleLocale = string;
export interface AppleMetadata {
version?: string;
copyright?: string;
info?: Record<AppleLocale, AppleInfo>;
categories?: AppleCategory;
@@ -18,13 +19,8 @@ export type AppleAdvisory = Partial<AgeRatingDeclarationProps>;
export type AppleCategory = (string | string[])[];
export interface AppleRelease {
isPhasedReleaseEnabled?: boolean;
shouldResetRatings?: boolean;
autoReleaseDate?: number | string;
automaticRelease?: boolean;
usesThirdPartyContent?: boolean;
/** Alternative to setting `ITSAppUsesNonExemptEncryption` in the binary's `Info.plist`. */
usesNonExemptEncryption?: boolean;
automaticRelease?: boolean | string;
phasedRelease?: boolean;
}
export interface AppleInfo {
+65
View File
@@ -1,9 +1,12 @@
import Ajv from 'ajv';
import assert from 'assert';
import fs from 'fs-extra';
import path from 'path';
import { AppleConfigReader } from './apple/config/reader';
import { AppleConfigWriter } from './apple/config/writer';
import { AppleMetadata } from './apple/types';
import { MetadataValidationError } from './errors';
export interface MetadataConfig {
/** The store configuration version */
@@ -12,6 +15,68 @@ export interface MetadataConfig {
apple?: AppleMetadata;
}
/**
* Resolve the dynamic config from the user.
* It supports methods, async methods, or objects (json).
*/
async function resolveDynamicConfigAsync(configFile: string): Promise<unknown> {
const userConfigOrFunction = await import(configFile).then(file => file.default ?? file);
return typeof userConfigOrFunction === 'function'
? await userConfigOrFunction()
: userConfigOrFunction;
}
/**
* Get the static configuration file path, based on the metadata context.
* This uses any custom name provided, but swaps out the extension for `.json`.
*/
export function getStaticConfigFilePath({
projectDir,
metadataPath,
}: {
projectDir: string;
metadataPath: string;
}): string {
const configFile = path.join(projectDir, metadataPath);
const configExtension = path.extname(configFile);
return path.join(projectDir, `${path.basename(configFile, configExtension)}.json`);
}
/**
* Load the store configuration from a metadata context.
* This can load `.json` and `.js` config files, using `require`.
* It throws MetadataValidationErrors when the file doesn't exist, or contains errors.
* The user is prompted to try anyway when errors are found.
*/
export async function loadConfigAsync({
projectDir,
metadataPath,
skipValidation = false,
}: {
projectDir: string;
metadataPath: string;
skipValidation?: boolean;
}): Promise<MetadataConfig> {
const configFile = path.join(projectDir, metadataPath);
if (!(await fs.pathExists(configFile))) {
throw new MetadataValidationError(`Metadata store config file not found: "${configFile}"`);
}
const configData = await resolveDynamicConfigAsync(configFile);
if (!skipValidation) {
const { valid, errors: validationErrors } = validateConfig(configData);
if (!valid) {
throw new MetadataValidationError(`Metadata store config errors found`, validationErrors);
}
}
return configData as MetadataConfig;
}
/**
* Run the JSON Schema validation to normalize defaults and flag early config errors.
* This includes validating the known store limitations for every configurable property.
+7 -6
View File
@@ -6,7 +6,7 @@ import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
import { createAppleTasks } from './apple/tasks';
import { createAppleWriter } from './config';
import { createAppleWriter, getStaticConfigFilePath } from './config';
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
import { MetadataDownloadError, MetadataValidationError } from './errors';
import { subscribeTelemetry } from './utils/telemetry';
@@ -16,15 +16,16 @@ import { subscribeTelemetry } from './utils/telemetry';
* Note, only App Store is supported at this time.
*/
export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promise<string> {
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath);
const filePath = getStaticConfigFilePath(metadataCtx);
const fileExists = await fs.pathExists(filePath);
if (fileExists) {
const filePathRelative = path.relative(metadataCtx.projectDir, filePath);
const overwrite = await confirmAsync({
message: `Do you want to overwrite the existing store configuration "${metadataCtx.metadataPath}"?`,
message: `Do you want to overwrite the existing "${filePathRelative}"?`,
});
if (!overwrite) {
throw new MetadataValidationError(`Store configuration already exists at "${filePath}"`);
throw new MetadataValidationError(`Store config already exists at "${filePath}"`);
}
}
@@ -35,7 +36,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
);
Log.addNewLineIfNone();
Log.log('Downloading App Store configuration...');
Log.log('Downloading App Store config...');
const errors: Error[] = [];
const config = createAppleWriter();
@@ -59,7 +60,7 @@ export async function downloadMetadataAsync(metadataCtx: MetadataContext): Promi
}
try {
await fs.writeJson(filePath, config.toSchema(), { spaces: 2 });
await fs.writeJSON(filePath, config.toSchema(), { spaces: 2 });
} finally {
unsubscribeTelemetry();
}
+31 -28
View File
@@ -1,12 +1,9 @@
import fs from 'fs-extra';
import path from 'path';
import { MetadataEvent } from '../analytics/events';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { AppleData } from './apple/data';
import { createAppleTasks } from './apple/tasks';
import { createAppleReader, validateConfig } from './config';
import { MetadataConfig, createAppleReader, loadConfigAsync } from './config';
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
import { subscribeTelemetry } from './utils/telemetry';
@@ -18,28 +15,7 @@ import { subscribeTelemetry } from './utils/telemetry';
export async function uploadMetadataAsync(
metadataCtx: MetadataContext
): Promise<{ appleLink: string }> {
const filePath = path.resolve(metadataCtx.projectDir, metadataCtx.metadataPath);
if (!(await fs.pathExists(filePath))) {
throw new MetadataValidationError(`Store configuration file not found "${filePath}"`);
}
const fileData = await fs.readJson(filePath);
const { valid, errors: validationErrors } = validateConfig(fileData);
if (!valid) {
const error = new MetadataValidationError(`Store configuration errors found`, validationErrors);
logMetadataValidationError(error);
Log.newLine();
Log.warn(
'Without further updates, the current store configuration may fail to be synchronized with the App Store or pass App Store review.'
);
const attempt = await confirmAsync({
message: 'Do you still want to push the store configuration?',
});
if (!attempt) {
throw error;
}
}
const storeConfig = await loadConfigWithValidationPromptAsync(metadataCtx);
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
MetadataEvent.APPLE_METADATA_UPLOAD,
@@ -50,8 +26,13 @@ export async function uploadMetadataAsync(
Log.log('Uploading App Store configuration...');
const errors: Error[] = [];
const config = createAppleReader(fileData);
const tasks = createAppleTasks(metadataCtx);
const config = createAppleReader(storeConfig);
const tasks = createAppleTasks(metadataCtx, {
// We need to resolve a different version as soon as possible.
// This version is the parent model of all changes we are going to push.
version: config.getVersion()?.versionString,
});
const taskCtx = { app };
for (const task of tasks) {
@@ -78,3 +59,25 @@ export async function uploadMetadataAsync(
return { appleLink: `https://appstoreconnect.apple.com/apps/${app.id}/appstore` };
}
async function loadConfigWithValidationPromptAsync(
metadataCtx: MetadataContext
): Promise<MetadataConfig> {
try {
return await loadConfigAsync(metadataCtx);
} catch (error) {
if (error instanceof MetadataValidationError) {
logMetadataValidationError(error);
Log.newLine();
Log.warn(
'Without further updates, the current store configuration can fail to be synchronized with the App Store or pass App Store review.'
);
if (await confirmAsync({ message: 'Do you still want to push the store configuration?' })) {
return await loadConfigAsync({ ...metadataCtx, skipValidation: true });
}
}
throw error;
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ export function ora(options?: Options | string): Ora {
// eslint-disable-next-line no-console
console.log = logReal;
// eslint-disable-next-line no-console
console.info = logReal;
console.info = infoReal;
// eslint-disable-next-line no-console
console.warn = warnReal;
// eslint-disable-next-line no-console
@@ -1,3 +1,4 @@
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { confirmAsync } from '../../prompts';
@@ -10,6 +11,7 @@ import {
findProjectRootAsync,
getProjectAccountName,
getProjectAccountNameAsync,
getProjectIdAsync,
promptToCreateProjectIfNotExistsAsync,
} from '../projectUtils';
@@ -248,3 +250,28 @@ describe(promptToCreateProjectIfNotExistsAsync, () => {
expect(projectId).toBe(null);
});
});
describe(getProjectIdAsync, () => {
it('gets the project ID from app config if exists', async () => {
await expect(
getProjectIdAsync({ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } })
).resolves.toEqual('1234');
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest.mocked(ensureProjectExistsAsync).mockImplementation(async () => '2345');
const projectId = await getProjectIdAsync({ name: 'test', slug: 'test' }, {}, { cwd: '/app' });
expect(projectId).toBe('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith('/app', {
extra: { eas: { projectId: '2345' } },
});
});
});
@@ -88,6 +88,7 @@ describe('MetadataJoi', () => {
expect(error).toBe(undefined);
});
});
describe(guessContentTypeFromExtension, () => {
it('returns the correct content type for jpg', () => {
expect(guessContentTypeFromExtension('jpg')).toBe('image/jpeg');
@@ -368,20 +369,8 @@ describe(uploadAssetsAsync, () => {
const iosBundlePath = uuidv4();
const dummyFilePath = uuidv4();
const userDefinedPath = uuidv4();
beforeAll(async () => {
await fs.writeFile(androidBundlePath, publishBundles.android.code);
await fs.writeFile(iosBundlePath, publishBundles.ios.code);
await fs.writeFile(dummyFilePath, dummyFileBuffer);
await fs.writeFile(userDefinedPath, 'I am an octet stream');
});
afterAll(async () => {
await fs.remove(androidBundlePath);
await fs.remove(iosBundlePath);
await fs.remove(dummyFilePath);
await fs.remove(userDefinedPath);
});
const testProjectId = uuidv4();
const expectedAssetLimit = 1400;
const userDefinedAsset = {
type: 'bundle',
@@ -407,10 +396,29 @@ describe(uploadAssetsAsync, () => {
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
},
};
beforeAll(async () => {
await fs.writeFile(androidBundlePath, publishBundles.android.code);
await fs.writeFile(iosBundlePath, publishBundles.ios.code);
await fs.writeFile(dummyFilePath, dummyFileBuffer);
await fs.writeFile(userDefinedPath, 'I am an octet stream');
});
afterAll(async () => {
await fs.remove(androidBundlePath);
await fs.remove(iosBundlePath);
await fs.remove(dummyFilePath);
await fs.remove(userDefinedPath);
});
jest.spyOn(PublishMutation, 'getUploadURLsAsync').mockImplementation(async () => {
return { specifications: ['{}', '{}', '{}'] };
});
jest
.spyOn(PublishQuery, 'getAssetLimitPerUpdateGroupAsync')
.mockImplementation(async () => expectedAssetLimit);
it('resolves if the assets are already uploaded', async () => {
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
const status = AssetMetadataStatus.Exists;
@@ -435,10 +443,11 @@ describe(uploadAssetsAsync, () => {
});
mockdate.set(0);
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toEqual({
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup, testProjectId)).resolves.toEqual({
assetCount: 6,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 0,
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
it('resolves if the assets are eventually uploaded', async () => {
@@ -467,12 +476,14 @@ describe(uploadAssetsAsync, () => {
});
mockdate.set(0);
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toEqual({
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup, testProjectId)).resolves.toEqual({
assetCount: 6,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 2,
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
it('updates spinner text throughout execution', async () => {
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
const status =
@@ -500,7 +511,7 @@ describe(uploadAssetsAsync, () => {
const updateSpinnerFn = jest.fn((_totalAssets, _missingAssets) => {});
mockdate.set(0);
await uploadAssetsAsync(assetsForUpdateInfoGroup, updateSpinnerFn);
await uploadAssetsAsync(assetsForUpdateInfoGroup, testProjectId, updateSpinnerFn);
const calls = updateSpinnerFn.mock.calls;
expect(calls).toEqual([
[3, 3],
+29 -11
View File
@@ -1,8 +1,10 @@
import { ExpoConfig, getConfigFilePaths, getPackageJson, modifyConfigAsync } from '@expo/config';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import pkgDir from 'pkg-dir';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import { AppPrivacy } from '../graphql/generated';
@@ -127,24 +129,19 @@ export async function setProjectIdAsync(
export async function getProjectIdAsync(
exp: ExpoConfig,
options: { env?: Env } = {}
options: { env?: Env } = {},
findProjectRootOptions: {
cwd?: string;
defaultToProcessCwd?: boolean;
} = {}
): Promise<string> {
if (!process.env.EAS_ENABLE_PROJECT_ID) {
const privacy = toAppPrivacy(exp.privacy);
return await ensureProjectExistsAsync({
accountName: getProjectAccountName(exp, await ensureLoggedInAsync()),
projectName: exp.slug,
privacy,
});
}
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
return localProjectId;
}
// Set the project ID if it is missing.
const projectDir = await findProjectRootAsync();
const projectDir = await findProjectRootAsync(findProjectRootOptions);
if (!projectDir) {
throw new Error('Please run this command inside a project directory.');
}
@@ -238,6 +235,27 @@ export function isExpoUpdatesInstalledOrAvailable(
return isExpoUpdatesInstalled(projectDir);
}
export async function validateAppVersionRuntimePolicySupportAsync(
projectDir: string,
exp: ExpoConfig
): Promise<void> {
if (typeof exp.runtimeVersion !== 'object' || exp.runtimeVersion?.policy !== 'appVersion') {
return;
}
const maybePackageJson = resolveFrom.silent(projectDir, 'expo-updates/package.json');
if (maybePackageJson) {
const { version } = await fs.readJson(maybePackageJson);
if (semver.gte(version, '0.14.4')) {
return;
}
}
Log.warn(
`You need to be on SDK 46 or higher, and use expo-updates >= 0.14.4 to use appVersion runtime policy.`
);
}
export async function installExpoUpdatesAsync(projectDir: string): Promise<void> {
Log.newLine();
Log.log(`Running ${chalk.bold('expo install expo-updates')}`);
+27 -9
View File
@@ -11,8 +11,9 @@ import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated'
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PresignedPost } from '../graphql/mutations/UploadSessionMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import { uploadWithPresignedPostAsync } from '../uploads';
import { uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import uniqBy from '../utils/expodash/uniqBy';
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
@@ -250,10 +251,12 @@ type AssetUploadResult = {
assetCount: number;
uniqueAssetCount: number;
uniqueUploadedAssetCount: number;
assetLimitPerUpdateGroup: number;
};
export async function uploadAssetsAsync(
assetsForUpdateInfoGroup: CollectedAssets,
projectId: string,
updateSpinnerText?: (totalAssets: number, missingAssets: number) => void
): Promise<AssetUploadResult> {
let assets: RawAsset[] = [];
@@ -283,25 +286,31 @@ export async function uploadAssetsAsync(
const totalAssets = uniqueAssets.length;
updateSpinnerText?.(totalAssets, totalAssets);
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(uniqueAssets);
const uniqueUploadedAssetCount = missingAssets.length;
const { specifications } = await PublishMutation.getUploadURLsAsync(
missingAssets.map(ma => ma.contentType)
);
const missingAssetChunks = chunk(missingAssets, 100);
const specifications: string[] = [];
for (const missingAssets of missingAssetChunks) {
const { specifications: chunkSpecifications } = await PublishMutation.getUploadURLsAsync(
missingAssets.map(ma => ma.contentType)
);
specifications.push(...chunkSpecifications);
}
updateSpinnerText?.(totalAssets, missingAssets.length);
const assetUploadPromiseLimit = promiseLimit(15);
await Promise.all(
const [assetLimitPerUpdateGroup] = await Promise.all([
PublishQuery.getAssetLimitPerUpdateGroupAsync(projectId),
missingAssets.map((missingAsset, i) => {
assetUploadPromiseLimit(async () => {
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
await uploadWithPresignedPostAsync(missingAsset.path, presignedPost);
await uploadWithPresignedPostWithRetryAsync(missingAsset.path, presignedPost);
});
})
);
}),
]);
let timeout = 1;
while (missingAssets.length > 0) {
@@ -317,5 +326,14 @@ export async function uploadAssetsAsync(
assetCount: assets.length,
uniqueAssetCount: uniqueAssets.length,
uniqueUploadedAssetCount,
assetLimitPerUpdateGroup,
};
}
export function isUploadedAssetCountAboveWarningThreshold(
uploadedAssetCount: number,
assetLimitPerUpdateGroup: number
): boolean {
const warningThreshold = Math.floor(assetLimitPerUpdateGroup * 0.75);
return uploadedAssetCount > warningThreshold;
}
@@ -1,13 +1,14 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { AppVersionSource, EasJsonReader } from '@expo/eas-json';
import { AppVersionSource, EasJson, EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import Log from '../log';
import { confirmAsync } from '../prompts';
import { ProfileData } from '../utils/profiles';
export async function ensureRemoteVersionPolicyAsync(
export async function ensureVersionSourceIsRemoteAsync(
projectDir: string,
easJsonReader: EasJsonReader
): Promise<void> {
@@ -36,7 +37,23 @@ export async function ensureRemoteVersionPolicyAsync(
Log.withTick('Updated eas.json');
}
export async function validateAppConfigForRemoteVersionPolicyAsync(exp: ExpoConfig): Promise<void> {
export function validateBuildProfileVersionSettings(
profileInfo: ProfileData<'build'>,
cliConfig: EasJson['cli']
): void {
if (cliConfig?.appVersionSource !== AppVersionSource.REMOTE) {
return;
}
if (profileInfo.profile.autoIncrement === 'version') {
throw new Error(
`${chalk.bold(
'{"autoIncrement": "version"}'
)} is not supported when app version source is set to remote.`
);
}
}
export function validateAppConfigForRemoteVersionSource(exp: ExpoConfig, platform: Platform): void {
if (typeof exp.runtimeVersion === 'object' && exp.runtimeVersion?.policy === 'nativeVersion') {
throw new Error(
`${chalk.bold('nativeVersion')} policy for ${chalk.bold(
@@ -46,18 +63,22 @@ export async function validateAppConfigForRemoteVersionPolicyAsync(exp: ExpoConf
)} or define version explicitly.`
);
}
if (exp.ios?.buildNumber !== undefined) {
throw new Error(
if (platform === Platform.IOS && exp.ios?.buildNumber !== undefined) {
Log.warn(
`${chalk.bold(
'ios.buildNumber'
)} field in app config is not supported when version source is set to remote, remove it and re-run the command.`
)} field in app config is ignored when version source is set to remote, but this value will still be in the manifest available via ${chalk.bold(
'expo-constants'
)}. It's recommended to remove this value from app config.`
);
}
if (exp.android?.versionCode !== undefined) {
throw new Error(
if (platform === Platform.ANDROID && exp.android?.versionCode !== undefined) {
Log.warn(
`${chalk.bold(
'android.versionCode'
)} field in app config is not supported when version source is set to remote, remove it and re-run the command.`
)} field in app config is ignored when version source is set to remote, but this value will still be in the manifest available via ${chalk.bold(
'expo-constants'
)}. It's recommended to remove this value from app config.`
);
}
}
+5 -3
View File
@@ -35,7 +35,7 @@ export async function submitAsync<T extends Platform>(
export async function waitToCompleteAsync(
submissions: SubmissionFragment[],
{ verbose = false }: { verbose?: boolean } = {}
): Promise<void> {
): Promise<SubmissionFragment[]> {
Log.newLine();
const completedSubmissions = await waitForSubmissionsEndAsync(submissions);
const moreSubmissions = completedSubmissions.length > 1;
@@ -60,7 +60,7 @@ export async function waitToCompleteAsync(
Log.newLine();
}
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
return completedSubmissions;
}
function printInstructionsForAndroidSubmission(submission: SubmissionFragment): void {
@@ -87,7 +87,9 @@ function printInstructionsForIosSubmission(submission: SubmissionFragment): void
}
}
function exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(submissions: SubmissionFragment[]): void {
export function exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(
submissions: SubmissionFragment[]
): void {
const nonFinishedSubmissions = submissions.filter(
({ status }) => status !== SubmissionStatus.Finished
);
+1 -7
View File
@@ -11,7 +11,6 @@ const APP_STORE_NAMES: Record<AppPlatform, string> = {
[AppPlatform.Ios]: 'Apple App Store Connect',
};
const CHECK_TIMEOUT_MS = 3600_000;
const CHECK_INTERVAL_MS = 5_000;
export async function waitForSubmissionsEndAsync(
@@ -25,9 +24,7 @@ export async function waitForSubmissionsEndAsync(
const spinner = ora(`Submitting`).start();
let time = new Date().getTime();
const timeoutTime = time + CHECK_TIMEOUT_MS;
while (time <= timeoutTime) {
while (true) {
const submissions = await Promise.all(
initialSubmissions.map(({ id }) => {
try {
@@ -76,11 +73,8 @@ export async function waitForSubmissionsEndAsync(
}
}
time = new Date().getTime();
await sleepAsync(CHECK_INTERVAL_MS);
}
spinner.warn('Timed out');
throw new Error('Timeout reached! It is taking longer than expected to complete, aborting...');
}
function getSingleSpinnerText(submission: SubmissionFragment): string {
+86 -24
View File
@@ -3,6 +3,7 @@ import FormData from 'form-data';
import fs from 'fs-extra';
import { Response } from 'node-fetch';
import nullthrows from 'nullthrows';
import promiseRetry from 'promise-retry';
import { URL } from 'url';
import fetch from './fetch';
@@ -18,7 +19,11 @@ export async function uploadFileAtPathToS3Async(
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(type);
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
const response = await uploadWithPresignedPostAsync(path, presignedPost, handleProgressEvent);
const response = await uploadWithPresignedPostWithProgressAsync(
path,
presignedPost,
handleProgressEvent
);
const location = nullthrows(
response.headers.get('location'),
`location does not exist in response headers (make sure you're uploading to AWS S3)`
@@ -27,11 +32,48 @@ export async function uploadFileAtPathToS3Async(
return { url, bucketKey: presignedPost.fields.key };
}
export async function uploadWithPresignedPostAsync(
export async function uploadWithPresignedPostWithRetryAsync(
file: string,
presignedPost: PresignedPost,
handleProgressEvent?: ProgressHandler
presignedPost: PresignedPost
): Promise<Response> {
return await promiseRetry(
async retry => {
// retry fetch errors (usually connection or DNS errors)
let response: Response;
try {
response = await uploadWithPresignedPostAsync(file, presignedPost);
} catch (e: any) {
return retry(e);
}
// retry 408, 429, 5xx as suggested by google
if (
response.status === 408 ||
response.status === 429 ||
(response.status >= 500 && response.status <= 599)
) {
return retry(new Error(`Presigned upload responded with a ${response.status} status`));
}
// don't retry other errors
if (!response.ok) {
throw new Error(`Presigned upload responded with a ${response.status} status`);
}
return response;
},
// retry parameters match google suggested defaults: https://cloud.google.com/storage/docs/retry-strategy#node.js
{
retries: 3,
factor: 2,
}
);
}
async function createPresignedPostFormDataAsync(
file: string,
presignedPost: PresignedPost
): Promise<{ form: FormData; fileSize: number }> {
const fileStat = await fs.stat(file);
const fileSize = fileStat.size;
const form = new FormData();
@@ -39,6 +81,30 @@ export async function uploadWithPresignedPostAsync(
form.append(fieldKey, fieldValue);
}
form.append('file', fs.createReadStream(file), { knownLength: fileSize });
return { form, fileSize };
}
async function uploadWithPresignedPostAsync(
file: string,
presignedPost: PresignedPost
): Promise<Response> {
const { form } = await createPresignedPostFormDataAsync(file, presignedPost);
const formHeaders = form.getHeaders();
return await fetch(presignedPost.url, {
method: 'POST',
body: form,
headers: {
...formHeaders,
},
});
}
async function uploadWithPresignedPostWithProgressAsync(
file: string,
presignedPost: PresignedPost,
handleProgressEvent: ProgressHandler
): Promise<Response> {
const { form, fileSize } = await createPresignedPostFormDataAsync(file, presignedPost);
const formHeaders = form.getHeaders();
const uploadPromise = fetch(presignedPost.url, {
method: 'POST',
@@ -49,27 +115,23 @@ export async function uploadWithPresignedPostAsync(
});
let currentSize = 0;
if (handleProgressEvent) {
form.addListener('data', (chunk: Buffer) => {
currentSize += Buffer.byteLength(chunk);
handleProgressEvent({
progress: {
total: fileSize,
percent: currentSize / fileSize,
transferred: currentSize,
},
});
form.addListener('data', (chunk: Buffer) => {
currentSize += Buffer.byteLength(chunk);
handleProgressEvent({
progress: {
total: fileSize,
percent: currentSize / fileSize,
transferred: currentSize,
},
});
try {
const response = await uploadPromise;
handleProgressEvent({ isComplete: true });
return response;
} catch (error: any) {
handleProgressEvent({ isComplete: true, error });
throw error;
}
} else {
return await uploadPromise;
});
try {
const response = await uploadPromise;
handleProgressEvent({ isComplete: true });
return response;
} catch (error: any) {
handleProgressEvent({ isComplete: true, error });
throw error;
}
}
+5 -2
View File
@@ -1,3 +1,4 @@
import * as PackageManagerUtils from '@expo/package-manager';
import spawnAsync from '@expo/spawn-async';
import { Errors } from '@oclif/core';
import chalk from 'chalk';
@@ -33,15 +34,17 @@ export default class GitClient extends Client {
Log.warn("It looks like you haven't initialized the git repository yet.");
Log.warn('EAS Build requires you to use a git repository for your project.');
const cwd = process.cwd();
const repoRoot = PackageManagerUtils.findWorkspaceRoot(cwd) ?? cwd;
const confirmInit = await confirmAsync({
message: `Would you like us to run 'git init' in the current directory for you?`,
message: `Would you like us to run 'git init' in ${repoRoot} for you?`,
});
if (!confirmInit) {
throw new Error(
'A git repository is required for building your project. Initialize it and run this command again.'
);
}
await spawnAsync('git', ['init']);
await spawnAsync('git', ['init'], { cwd: repoRoot });
Log.log("We're going to make an initial commit for your repository.");
@@ -1,6 +1,8 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import path from 'path';
import Log from '../../log';
import { Ignore, makeShallowCopyAsync } from '../local';
import GitClient from './git';
@@ -25,4 +27,18 @@ export default class GitNoCommitClient extends GitClient {
await ignore.initIgnoreAsync();
return ignore.ignores(filePath);
}
public async trackFileAsync(file: string): Promise<void> {
try {
await super.trackFileAsync(file);
} catch {
// In the no commit workflow it doesn't matter if we fail to track changes,
// so we can ignore if this throws an exception
Log.warn(
`Unable to track ${chalk.bold(path.basename(file))} in Git. Proceeding without tracking.`
);
Log.warn(` Reason: the command ${chalk.bold(`"git add ${file}"`)} exited with an error.`);
Log.newLine();
}
}
}
+9 -5
View File
@@ -1,11 +1,11 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "0.57.0",
"version": "1.0.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/eas-build-job": "0.2.84",
"@expo/eas-build-job": "0.2.85",
"@expo/json-file": "8.2.36",
"chalk": "4.1.2",
"env-string": "1.0.1",
@@ -17,7 +17,8 @@
},
"devDependencies": {
"@types/fs-extra": "9.0.13",
"memfs": "3.4.3",
"memfs": "3.4.7",
"rimraf": "3.0.2",
"typescript": "4.7.4"
},
"engines": {
@@ -32,10 +33,13 @@
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "rm -rf build && yarn build",
"prepack": "rimraf build && yarn build",
"test": "jest",
"clean": "rm -rf build node_modules yarn-error.log"
"clean": "rimraf build node_modules yarn-error.log"
},
"files": [
"/build"
],
"publishConfig": {
"access": "public"
},
+483
View File
@@ -0,0 +1,483 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"description": "The EAS config (eas.json) validation and documentation.",
"meta": {
"updatedAt": "2022-07-18T15:00:00.000Z"
},
"properties": {
"cli": {
"$ref": "#/definitions/Cli"
},
"build": {
"$ref": "#/definitions/Build"
},
"submit": {
"$ref": "#/definitions/Submit"
}
},
"definitions": {
"Cli": {
"type": "object",
"description": "Configure the basic behavior of your project with the EAS CLI",
"properties": {
"version": {
"type": "string",
"description": "The compatible versions of EAS CLI with this config",
"examples": [
">=0.54.1"
]
},
"requireCommit": {
"type": "boolean",
"description": "If all changes required to be committed before building or submitting"
},
"appVersionSource": {
"enum": [
"local",
"remote"
],
"markdownDescription": "Version policy defines whether version of your app should be based on your local project or values stored on EAS servers (remote).\n\nThis is the configuration required for `eas build:version:set` and works with the `autoIncrement` options per platform.",
"markdownEnumDescriptions": [
"When using local, the `autoIncrement` is based on your local project values.",
"When using remote, the `autoIncrement` is based on the values stored on EAS servers."
]
}
}
},
"Build": {
"type": "object",
"description": "EAS Build configuration. Learn more: https://docs.expo.dev/build-reference/eas-json/",
"markdownDescription": "EAS Build configuration. [Learn more](https://docs.expo.dev/build-reference/eas-json/)",
"patternProperties": {
".*": {
"$ref": "#/definitions/BuildProfile"
}
}
},
"BuildCache": {
"type": "object",
"properties": {
"disabled": {
"type": "boolean",
"description": "Disables caching. Defaults to false."
},
"key": {
"type": "string",
"description": "Cache key. You can invalidate the cache by changing this value."
},
"customPaths": {
"type": "array",
"description": "List of the paths that will be saved after a successful build and restored at the beginning of the next one.",
"markdownDescription": "List of the paths that will be saved after a successful build and restored at the beginning of the next one. Both absolute and relative paths are supported, where relative paths are resolved from the directory with `eas.json`.",
"uniqueItems": true,
"items": {
"type": "string"
}
},
"cacheDefaultPaths": {
"type": "boolean",
"description": "Specifies whether to cache the recommended set of files, currently only Podfile.lock is cached by default for iOS build and nothing is cached for Android.",
"default": true
}
}
},
"BuildProfile": {
"type": "object",
"description": "The build profile configuration.",
"properties": {
"extends": {
"type": "string",
"description": "The name of the build profile that the current one should inherit values from. This value can't be specified per platform."
},
"credentialsSource": {
"enum": [
"remote",
"local"
],
"description": "The source of credentials used to sign build artifacts. Learn more: https://docs.expo.dev/app-signing/local-credentials/",
"markdownDescription": "The source of credentials used to sign build artifacts.\n\n- `remote` - if you want to use the credentials managed by EAS.\n- `local` - if you want to provide your own `credentials.json` file. [learn more](https://docs.expo.dev/app-signing/local-credentials/)",
"default": "remote",
"markdownEnumDescriptions": [
"Use the credentials managed by EAS.",
"Provide your own `credentials.json` file. [learn more](https://docs.expo.dev/app-signing/local-credentials/)"
]
},
"releaseChannel": {
"type": "string",
"description": "Name of the release channel for the `expo-updates` package. Learn more: https://docs.expo.dev/distribution/release-channels/",
"markdownDescription": "Name of the release channel for the `expo-updates` package. [Learn more](https://docs.expo.dev/distribution/release-channels/)\n\nIf you do not specify a channel, your binary will pull releases from the `default` channel. If you do not use `expo-updates` in your project then this property will have no effect.\n\n**This field only applies to the Classic Update service**; if you use EAS Update, use the [channel](https://docs.expo.dev/build-reference/eas-json/#channel) field instead.",
"pattern": "^[a-z0-9._-]+$",
"patternErrorMessage": "Release channel names can only contain lowercase letters, numbers and special characters `.`, `_` and `-`."
},
"channel": {
"type": "string",
"description": "The channel is a name we can give to multiple builds to identify them easily. Learn more: https://docs.expo.dev/eas-update/how-eas-update-works/",
"markdownDescription": "The channel is a name we can give to multiple builds to identify them easily. [Learn more](https://docs.expo.dev/eas-update/how-eas-update-works/)\n\n**This field only applies to the EAS Update service**, if your project still uses Classic Updates then use the [releaseChannel](https://docs.expo.dev/build-reference/eas-json/#releasechannel) field instead."
},
"distribution": {
"enum": [
"store",
"internal"
],
"description": "The method of distributing your app. Learn more: https://docs.expo.dev/build/internal-distribution/",
"markdownDescription": "The method of distributing your app.\n\n- `internal` - with this option you'll be able to share your build URLs with anyone, and they will be able to install the builds to their devices straight from the Expo website. When using `internal`, make sure the build produces an APK or IPA file. Otherwise, the shareable URL will be useless. [Learn more](https://docs.expo.dev/build/internal-distribution/)\n- `store` - produces builds for store uploads, your build URLs won't be shareable.",
"markdownEnumDescriptions": [
"With this option you'll be able to share your build URLs with anyone, and they will be able to install the builds to their devices straight from the Expo website. When using `internal`, make sure the build produces an APK or IPA file. Otherwise, the shareable URL will be useless. [Learn more](https://docs.expo.dev/build/internal-distribution/)",
"Produces builds for store uploads, your build URLs won't be shareable."
]
},
"developmentClient": {
"type": "boolean",
"description": "If set to true, this field expresses the intent to produce a development client build. Learn more: https://docs.expo.dev/development/introduction/",
"markdownDescription": "If set to true, this field expresses the intent to produce a development client build.\n\nFor the build to be successful, the project must have `expo-dev-client` installed and configured.\n> This field is sugar for setting the iOS `buildConfiguration` to `Debug` and Android `gradleCommand` to `:app:assembleDebug`. Those fields, if provided for the same build profile, will take precedence.\n\n[Learn more](https://docs.expo.dev/development/introduction/)",
"default": false
},
"prebuildCommand": {
"type": "string",
"description": "Optional override of the prebuild command used by EAS. Learn more: https://docs.expo.dev/workflow/expo-cli/#expo-prebuild",
"markdownDescription": "Optional override of the prebuild command used by EAS.\n\nFor example, you can specify `prebuild --template example-template` to use a custom template.\n>`--platform` and `--non-interactive` will be added automatically by the build engine, so you do not need to specify them manually.\n\n[Learn more](https://docs.expo.dev/workflow/expo-cli/#expo-prebuild)"
},
"node": {
"type": "string",
"description": "The version of Node.js to use for the build. See: https://nodejs.org/en/about/releases/",
"markdownDescription": "The exact version of [Node.js](https://nodejs.org/en/about/releases/) to use for the build."
},
"yarn": {
"type": "string",
"description": "The version of Yarn to use for the build. See: https://www.npmjs.com/package/yarn",
"markdownDescription": "The exact version of [Yarn](https://www.npmjs.com/package/yarn) to use for the build."
},
"expoCli": {
"type": "string",
"description": "The version of `expo-cli` used to `prebuild` your app.",
"markdownDescription": "Version of [expo-cli](https://www.npmjs.com/package/expo-cli) used to [prebuild](https://docs.expo.dev/workflow/expo-cli/#expo-prebuild) your app. It does not have any effect on bare projects."
},
"env": {
"type": "object",
"description": "Environment variables that should be set during the build process.\n\nShould only be used for values that you would commit to your git repository, i.e. not passwords or secrets.",
"patternProperties": {
".*": {
"type": "string",
"description": "This environment variable will be set during the build process."
}
}
},
"cache": {
"$ref": "#/definitions/BuildCache",
"description": "The cache configuration.",
"markdownDescription": "Cache configuration. This feature is intended for caching values that require a lot of computation, e.g. compilation results (both final binaries and any intermediate files), but it wouldn't work well for `node_modules` because the cache is not local to the machine, so the download speed is similar to downloading from the npm registry."
},
"android": {
"$ref": "#/definitions/BuildProfileAndroid"
},
"ios": {
"$ref": "#/definitions/BuildProfileIos"
}
}
},
"BuildProfileAndroid": {
"type": "object",
"description": "The Android-specific build profile configuration.",
"properties": {
"withoutCredentials": {
"type": "boolean",
"description": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository.",
"markdownDescription": "When set to `true`, EAS CLI won't require you to configure credentials when building the app. This comes in handy when you want to build debug binaries and the debug keystore is checked in to the repository.",
"default": false
},
"image": {
"description": "Image with build environment. Learn more: https://docs.expo.dev/build-reference/infrastructure/",
"markdownDescription": "Image with build environment. [Learn more](https://docs.expo.dev/build-reference/infrastructure/)",
"default": "default",
"anyOf": [
{
"enum": [
"default",
"latest",
"stable",
"ubuntu-22.04-jdk-11-ndk-r21e",
"ubuntu-22.04-jdk-8-ndk-r21e",
"ubuntu-20.04-jdk-11-ndk-r21e",
"ubuntu-20.04-jdk-8-ndk-r21e",
"ubuntu-18.04-jdk-11-ndk-r19c",
"ubuntu-18.04-jdk-8-ndk-r19c"
],
"markdownEnumDescriptions": [
"- React Native `>=0.68.0` - `ubuntu-20.04-jdk-11-ndk-r21e`\n- React Native `<0.68.0` - `ubuntu-18.04-jdk-8-ndk-r19c`",
"`ubuntu-22.04-jdk-11-ndk-r21e`",
"`ubuntu-20.04-jdk-11-ndk-r21e`"
]
},
{
"type": "string"
}
]
},
"ndk": {
"type": "string",
"description": "The version of Android NDK."
},
"autoIncrement": {
"enum": [
false,
true,
"versionCode",
"version"
],
"description": "Controls how EAS CLI bumps your application build version.",
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n- `\"version\"` - the patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`).\n- `\"versionCode\"` (or `true`) - `expo.android.versionCode` is bumped (e.g. `3` -> `4`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `versionName` and `expo.android.versionCode` to `versionCode` in the `build.gradle`. Google Play uses these values to identify the app build, `versionName` is the version visible to users, whereas `versionCode` defines the version number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
"default": false,
"markdownEnumDescriptions": [
"Versions won't be bumped automatically (default).",
"`expo.android.versionCode` is bumped (e.g. `3` -> `4`).",
"`expo.android.versionCode` is bumped (e.g. `3` -> `4`).",
"The patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`)."
]
},
"buildType": {
"enum": ["app-bundle", "apk"],
"description": "Type of the artifact you want to build.",
"markdownDescription": "Type of the artifact you want to build. It controls what Gradle task will be used, can be overridden by `gradleCommand` or `developmentClient: true` option.\n- `app-bundle` - `:app:bundleRelease`\n- `apk` - `:app:assembleRelease`",
"markdownEnumDescriptions": [
"Gradle task `:app:bundleRelease` is used",
"Gradle task `:app:assembleRelease` is used"
]
},
"gradleCommand": {
"type": "string",
"description": "Gradle task that will be used to build your project, e.g. `:app:assembleDebug` to build a debug binary.",
"markdownDescription": "Gradle task that will be used to build your project, e.g. `:app:assembleDebug` to build a debug binary.\n\nIt's not recommended unless you need to run a task that `buildType` does not support, it takes priority over `buildType` and `developmentClient`."
},
"artifactPath": {
"type": "string",
"description": "Path (or pattern) where EAS Build is going to look for the build artifacts. See: https://github.com/mrmlnc/fast-glob#pattern-syntax",
"markdownDescription": "Path (or pattern) where EAS Build is going to look for the build artifacts. EAS Build uses the `fast-glob` npm package for pattern matching ([see their README to learn more about the syntax you can use](https://github.com/mrmlnc/fast-glob#pattern-syntax)).",
"default": "android/app/build/outputs/**/*.{apk,aab}"
}
}
},
"BuildProfileIos": {
"type": "object",
"description": "The iOS-specific build profile configuration.",
"properties": {
"simulator": {
"type": "boolean",
"description": "If set to true, creates build for simulator.",
"default": false
},
"enterpriseProvisioning": {
"enum": ["universal", "adhoc"],
"markdownDescription": "Provisioning method used for `\"distribution\": \"internal\"` when you have an Apple account with Apple Developer Enterprise Program membership.\n\nYou can choose if you want to use `adhoc` or `universal` provisioning. The latter is recommended as it does not require you to register each individual device. If you don't provide this option and you still authenticate with an enterprise team, you'll be prompted which provisioning method to use.",
"markdownEnumDescriptions": [
"Recommended as it does not require you to register each individual device"
]
},
"autoIncrement": {
"enum": [
false,
true,
"buildNumber",
"version"
],
"description": "Controls how EAS CLI bumps your application build version.",
"markdownDescription": "Controls how EAS CLI bumps your application build version.\n\nAllowed values:\n\n- `\"version\"` - the patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`).\n- `\"buildNumber\"` (or `true`) - the last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`).\n- `false` - versions won't be bumped automatically (default)\n\nIn the case of a bare project, it also updates versions in native code. `expo.version` corresponds to `CFBundleShortVersionString` and `expo.ios.buildNumber` to `CFBundleVersion` in the `Info.plist`. The App Store is using those values to identify the app build, `CFBundleShortVersionString` is the version visible to users, whereas `CFBundleVersion` defines the build number. The combination of those needs to be unique, so you can bump either of them.\n\nThis feature is not intended for use with dynamic configuration (app.config.js). EAS CLI will throw an error if you don't use app.json.",
"default": false,
"markdownEnumDescriptions": [
"Versions won't be bumped automatically (default)",
"The last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`)",
"The last component of `expo.ios.buildNumber` is bumped (e.g. `1.2.3.39` -> `1.2.3.40`)",
"The patch of `expo.version` is bumped (e.g. `1.2.3` -> `1.2.4`)"
]
},
"image": {
"description": "Image with build environment. Learn more: https://docs.expo.dev/build-reference/infrastructure/",
"markdownDescription": "Image with build environment. [Learn more](https://docs.expo.dev/build-reference/infrastructure/)",
"default": "default",
"anyOf": [
{
"enum": [
"default",
"latest",
"stable",
"macos-monterey-12.4-xcode-13.4",
"macos-monterey-12.3-xcode-13.3",
"macos-monterey-12.1-xcode-13.2",
"macos-big-sur-11.4-xcode-13.0",
"macos-big-sur-11.4-xcode-12.5"
],
"markdownEnumDescriptions": [
"- Expo SDK `>=45` - `macos-monterey-12.3-xcode-13.3`\n- Expo SDK `<45` - `macos-big-sur-11.4-xcode-13.0`",
"`macos-monterey-12.4-xcode-13.4`",
"`macos-monterey-12.3-xcode-13.3`"
]
},
{
"type": "string"
}
]
},
"bundler": {
"type": "string",
"description": "The version of bundler to use for the build. See: https://bundler.io/.",
"markdownDescription": "The version of [bundler](https://bundler.io/) to use for the build."
},
"fastlane": {
"type": "string",
"description": "The version of fastlane to use for the build. See: https://fastlane.tools/",
"markdownDescription": "The version of [fastlane](https://fastlane.tools/) to use for the build."
},
"cocoapods": {
"type": "string",
"description": "The version of CocoaPods to use for the build. See: https://cocoapods.org/",
"markdownDescription": "The version of [CocoaPods](https://cocoapods.org/) to use for the build."
},
"scheme": {
"type": "string",
"description": "Xcode project's scheme.",
"markdownDescription": "Xcode project's scheme.\n- managed project: does not have any effect\n- bare project\n - If your project has multiple schemes, you should set this value.\n - If the project has only one scheme, it will be detected automatically.\n - If multiple schemes exist and this value is **not** set, EAS CLI will prompt you to select one of them."
},
"buildConfiguration": {
"type": "string",
"description": "Xcode project's Build Configuration.",
"markdownDescription": "Xcode project's Build Configuration.\n- managed project: \"Release\" or \"Debug\", defaults to \"Release\"\n- bare project: defaults to the value specified in the scheme\n\nIt takes priority over `developmentClient` field."
},
"artifactPath": {
"type": "string",
"description": "Path (or pattern) where EAS Build is going to look for the build artifacts. See: https://github.com/mrmlnc/fast-glob#pattern-syntax",
"markdownDescription": "Path (or pattern) where EAS Build is going to look for the build artifacts.\n\nEAS Build uses the `fast-glob` npm package for pattern matching, ([See their README to learn more about the syntax you can use](https://github.com/mrmlnc/fast-glob#pattern-syntax)).\n\nYou should modify that path only if you are using a custom `Gymfile`.\n\nThe default is `ios/build/Build/Products/*-iphonesimulator/*.app` when building for simulator and `ios/build/*.ipa` in other cases."
}
}
},
"Submit": {
"type": "object",
"description": "EAS Submit configuration. Learn more: https://docs.expo.dev/submit/eas-json/",
"markdownDescription": "EAS Submit configuration. [Learn more](https://docs.expo.dev/submit/eas-json/)",
"patternProperties": {
".*": {
"$ref": "#/definitions/SubmitProfile",
"description": "The submit profile name with the configuration."
}
}
},
"SubmitProfile": {
"type": "object",
"properties": {
"extends": {
"type": "string",
"description": "The name of the submit profile that the current one should inherit values from."
},
"android": {
"$ref": "#/definitions/SubmitProfileAndroid"
},
"ios": {
"$ref": "#/definitions/SubmitProfileIos"
}
}
},
"SubmitProfileAndroid": {
"type": "object",
"description": "The Android-specific submit profile configuration.",
"properties": {
"serviceAccountKeyPath": {
"type": "string",
"description": "Path to the JSON file with service account key used to authenticate with Google Play. Learn more: https://expo.fyi/creating-google-service-account",
"markdownDescription": "Path to the JSON file with service account key used to authenticate with Google Play. [Learn more](https://expo.fyi/creating-google-service-account)"
},
"track": {
"enum": ["beta", "alpha", "internal", "production"],
"description": "The track of the application to use. Learn more: https://support.google.com/googleplay/android-developer/answer/9859348?hl=en",
"markdownDescription": "The [track of the application](https://support.google.com/googleplay/android-developer/answer/9859348?hl=en) to use.",
"markdownEnumDescriptions": [
"**Open testing** releases are available to testers on Google Play. Users can join tests from your store listing.",
"**Closed testing** releases are available to a limited number of testers that you choose, who can test a pre-release version of your app and submit feedback.",
"**Internal testing** releases are available to up to 100 testers that you choose.",
"**Production releases** are available to all Google Play users in your chosen countries."
]
},
"releaseStatus": {
"enum": ["draft", "inProgress", "halted", "completed"],
"description": "The status of a release. Learn more: https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks",
"markdownDescription": "The status of a release. [Learn more](https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks)",
"enumDescriptions": [
"The release's APKs are not being served to users.",
"The release's APKs are being served to a fraction of users, if set.",
"The release's APKs will no longer be served to users. Users who already have these APKs are unaffected.",
"The release will have no further changes. Its APKs are being served to all users, unless they are eligible to APKs of a more recent release."
]
},
"changesNotSentForReview": {
"type": "boolean",
"description": "Indicates that the changes sent with this submission will not be reviewed until they are explicitly sent for review from the Google Play Console UI. Defaults to false.",
"default": false
},
"applicationId": {
"type": "string",
"description": "The application id that will be used when accessing Service Account Keys managed by Expo, it does not have any effect if you are using local credentials. In most cases this value will be autodetected, but if you have multiple product flavors, this value might be necessary."
}
}
},
"SubmitProfileIos": {
"type": "object",
"description": "The iOS-specific submit profile configuration.",
"properties": {
"appleId": {
"type": "string",
"description": "Your Apple ID username (you can also set the `EXPO_APPLE_ID` env variable).",
"markdownDescription": "Your Apple ID username (you can also set the `EXPO_APPLE_ID` env variable)."
},
"ascAppId": {
"type": "string",
"description": "App Store Connect unique application Apple ID number. When set, results in skipping the app creation step. Learn more: https://expo.fyi/asc-app-id",
"markdownDescription": "App Store Connect unique application Apple ID number. When set, results in skipping the app creation step. [Learn more](https://expo.fyi/asc-app-id)"
},
"appleTeamId": {
"type": "string",
"description": "Your Apple Developer Team ID."
},
"sku": {
"type": "string",
"description": "An unique ID for your app that is not visible on the App Store, will be generated unless provided."
},
"language": {
"type": "string",
"description": "Primary language. Defaults to `en-US`.",
"markdownDescription": "Primary language. Defaults to `en-US`."
},
"companyName": {
"type": "string",
"description": "The name of your company, needed only for the first submission of any app to the App Store."
},
"appName": {
"type": "string",
"description": "The name of your app as it will appear on the App Store. Defaults to `expo.name` from the app config.",
"markdownDescription": "The name of your app as it will appear on the App Store. Defaults to `expo.name` from the app config."
},
"ascApiKeyPath": {
"type": "string",
"description": "The path to your App Store Connect Api Key .p8 file. Learn more: https://expo.fyi/creating-asc-api-key",
"markdownDescription": "The path to your App Store Connect Api Key .p8 file. [Learn more](https://expo.fyi/creating-asc-api-key)"
},
"ascApiKeyIssuerId": {
"type": "string",
"description": "The Issuer ID of your App Store Connect Api Key. Learn more: https://expo.fyi/creating-asc-api-key",
"markdownDescription": "The Issuer ID of your App Store Connect Api Key. [Learn more](https://expo.fyi/creating-asc-api-key)"
},
"ascApiKeyId": {
"type": "string",
"description": "The Key ID of your App Store Connect Api Key. Learn more: https://expo.fyi/creating-asc-api-key",
"markdownDescription": "The Key ID of your App Store Connect Api Key. [Learn more](https://expo.fyi/creating-asc-api-key)"
},
"bundleIdentifier": {
"type": "string",
"description": "The Bundle identifier that will be used when accessing submit credentials managed by Expo, it does not have any effect if you are using local credentials. In most cases this value will be autodetected, but if you have multiple Xcode schemes and targets, this value might be necessary."
},
"metadataPath": {
"type": "string",
"description": "The path to your store configuration file. Learn more: https://docs.expo.dev/eas-metadata/introduction/",
"markdownDescription": "The path to your store configuration file. [Learn more](https://docs.expo.dev/eas-metadata/introduction/)"
}
}
}
}
}
Generated
+1795 -1648
View File
File diff suppressed because it is too large. Load diff