mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
62
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f33d859d75 | ||
|
|
69515ab47b | ||
|
|
ac5f7ab186 | ||
|
|
265c7eacc5 | ||
|
|
98229581ca | ||
|
|
a718fd10d3 | ||
|
|
78a63b8ca1 | ||
|
|
7487f413de | ||
|
|
da5f4e644a | ||
|
|
be38117012 | ||
|
|
8f3314f188 | ||
|
|
31b6cc9ab6 | ||
|
|
04a8b08f59 | ||
|
|
8bce9b19b2 | ||
|
|
d3d6d41f9f | ||
|
|
0534328dfc | ||
|
|
d0b9af2553 | ||
|
|
80674a3451 | ||
|
|
41b48a80be | ||
|
|
3d5b77a263 | ||
|
|
fa25d259d9 | ||
|
|
26596f32e9 | ||
|
|
031c610d58 | ||
|
|
fcb5beccb4 | ||
|
|
3003a712f6 | ||
|
|
e89f030c29 | ||
|
|
3e224f9583 | ||
|
|
db338b2b78 | ||
|
|
971c8e4010 | ||
|
|
df64a59354 | ||
|
|
e5d80fcb6d | ||
|
|
4088ea3e43 | ||
|
|
edd42186d9 | ||
|
|
e6ba63cfd0 | ||
|
|
7b60901bb1 | ||
|
|
a551e9010a | ||
|
|
95441798ed | ||
|
|
814f863a63 | ||
|
|
283f086b25 | ||
|
|
7da01333c6 | ||
|
|
c057eaa098 | ||
|
|
968fdd221c | ||
|
|
6d28e0eecc | ||
|
|
a2b4d2dbd2 | ||
|
|
660c996602 | ||
|
|
35ede265ed | ||
|
|
cf02f075d7 | ||
|
|
a2027eabd2 | ||
|
|
4ef239c59c | ||
|
|
3610eee2b5 | ||
|
|
54ae94f532 | ||
|
|
c68bf3e26f | ||
|
|
940e3f6d40 | ||
|
|
4c6226b90b | ||
|
|
1341bb18d1 | ||
|
|
beac65ce53 | ||
|
|
a8cc54d409 | ||
|
|
6a639bd556 | ||
|
|
dde9685b38 | ||
|
|
5b3a76a5b5 | ||
|
|
513e09157b | ||
|
|
69a939c635 |
No files matched your search
@@ -37,7 +37,7 @@ body:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx expo-env-info` and paste the output here.
|
||||
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
name: Issue Triage
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
needs-repro:
|
||||
runs-on: ubuntu-20.04
|
||||
if: "${{ contains(github.event.label.name, 'incomplete issue: missing or invalid repro') }}"
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Hi there! It looks like your issue requires a minimal reproducible example, but it is invalid or absent. Please prepare such an example and share it in a new issue.
|
||||
|
||||
**The best way to get attention to your issue is to provide a clean and easy way for a developer to reproduce the issue on their own machine.** Please do not provide your entire project, or a project with more code than is necessary to reproduce the issue.
|
||||
|
||||
A side benefit of going through the process of narrowing down the minimal amount of code needed to reproduce the issue is that you may get lucky and discover that the bug is due to a mistake in your application code that you can quickly fix on your own.
|
||||
|
||||
### Resources
|
||||
|
||||
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
|
||||
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
|
||||
|
||||
### Common concerns
|
||||
|
||||
#### "I've only been able to reproduce it in private, proprietary code"
|
||||
|
||||
You may not have spent enough time narrowing down the root cause of the issue. Try out the techniques discussed in this [manual debugging guide](https://expo.fyi/manual-debugging) to learn how to isolate the problem from the rest of your codebase.
|
||||
|
||||
#### "I didn't have time to create one"
|
||||
|
||||
That's understandable, it can take some time to prepare. We ask that you hold off on filing an issue until you are able to fully complete the required fields in the issue template.
|
||||
|
||||
#### "You can reproduce it by yourself by creating a project and following these steps"
|
||||
|
||||
This is useful knowledge, but it's still valuable to have the resulting project that is produced from running the steps, where you have verified you can reproduce the issue.
|
||||
`})
|
||||
github.issues.update({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
needs-info:
|
||||
runs-on: ubuntu-20.04
|
||||
if: "${{ contains(github.event.label.name, 'incomplete issue: missing info') }}"
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Hello! There isn't enough information provided in your issue description for us to be able to help you. The best way to get help is to provide information to enable other developers to understand the issue and reproduce it on their own machines.
|
||||
|
||||
Please create a new issue and fill out the entire issue template to the best of your ability. Refer to the following resources for more information on how to create a good issue report.
|
||||
|
||||
### Resources
|
||||
|
||||
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
|
||||
- ["How to create a Minimal, Reproducible Example"](https://stackoverflow.com/help/minimal-reproducible-example)
|
||||
- ["How to narrow down the source of an error"](https://expo.fyi/manual-debugging)
|
||||
`})
|
||||
github.issues.update({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
issue-accepted:
|
||||
runs-on: ubuntu-20.04
|
||||
if: github.event.label.name == 'issue accepted'
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Thank you for filing this issue!
|
||||
This comment acknowledges we believe this may be a bug and there’s enough information to investigate it.
|
||||
However, we can’t promise any sort of timeline for resolution. We prioritize issues based on severity, breadth of impact, and alignment with our roadmap. If you’d like to help move it more quickly, you can continue to investigate it more deeply and/or you can open a pull request that fixes the cause.
|
||||
`})
|
||||
|
||||
question:
|
||||
runs-on: ubuntu-20.04
|
||||
if: "${{ contains(github.event.label.name, 'invalid issue: question') }}"
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Hello! Our GitHub issues are reserved for bug reports.
|
||||
|
||||
If you have a question about Expo or related tools, please post on our forums at https://forums.expo.dev/ or join our Discord at https://chat.expo.dev/.
|
||||
|
||||
### Resources
|
||||
|
||||
- ["How do I ask a good question?"](https://stackoverflow.com/help/how-to-ask)
|
||||
- ["Join the community"](https://docs.expo.dev/next-steps/community/)
|
||||
`})
|
||||
github.issues.update({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
})
|
||||
|
||||
feature-request:
|
||||
runs-on: ubuntu-20.04
|
||||
if: "${{ contains(github.event.label.name, 'invalid issue: feature request') }}"
|
||||
steps:
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{ secrets.EXPO_BOT_GITHUB_TOKEN }}
|
||||
script: |
|
||||
github.issues.createComment({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
body: `Hello! Our GitHub issues are reserved for bug reports.
|
||||
|
||||
If you would like to request a feature, please post to https://expo.canny.io/feature-requests.
|
||||
`})
|
||||
github.issues.update({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'closed'
|
||||
})
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Test scripts
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: scripts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'scripts/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'scripts/**'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
@@ -27,6 +27,10 @@ jobs:
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
if: ${{ matrix.coverage }}
|
||||
env:
|
||||
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
||||
# 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
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
packages/eas-cli/tmp
|
||||
scripts/build
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
.vscode
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -10,7 +10,93 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Show spinner instead of silently timing out during asset upload. ([#1206](https://github.com/expo/eas-cli/pull/1206) by [@wschurman](https://github.com/wschurman))
|
||||
- Fix build archive S3 URLs. ([#1207](https://github.com/expo/eas-cli/pull/1207) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Increase max number of chained "extends" in build profiles. ([#1208](https://github.com/expo/eas-cli/pull/1208) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.55.1](https://github.com/expo/eas-cli/releases/tag/v0.55.1) - 2022-07-12
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix asset upload crash. ([#1205](https://github.com/expo/eas-cli/pull/1205) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [0.55.0](https://github.com/expo/eas-cli/releases/tag/v0.55.0) - 2022-07-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00/))
|
||||
- Allow users to skip metadata validation. ([#1175](https://github.com/expo/eas-cli/pull/1175) by [@byCedric](https://github.com/byCedric))
|
||||
- Add experimental `--resource-class` flag for Build commands. ([#1138](https://github.com/expo/eas-cli/pull/1138) by [@christopherwalter](https://github.com/christopherwalter))
|
||||
- Truncate long messages for `eas update` command, rather than failing. ([#1178](https://github.com/expo/eas-cli/pull/1178) by [@kgc00](https://github.com/kgc00))
|
||||
- Add review details to metadata store configuration. ([#1184](https://github.com/expo/eas-cli/pull/1184) by [@byCedric](https://github.com/byCedric))
|
||||
- Override applicationId via env. ([#1203](https://github.com/expo/eas-cli/pull/1203) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix submission archive URLs. ([#1179](https://github.com/expo/eas-cli/pull/1179) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update validation rules for metadata info locales. ([#1174](https://github.com/expo/eas-cli/pull/1174) by [@byCedric](https://github.com/byCedric))
|
||||
- Improve store configuration defaults and schema documentation. ([#1183](https://github.com/expo/eas-cli/pull/1183) by [@byCedric](https://github.com/byCedric))
|
||||
- Improve store config categories configuration. ([#1187](https://github.com/expo/eas-cli/pull/1187) by [@byCedric](https://github.com/byCedric))
|
||||
- Remove deprecated idfa and price tier from store config. ([#1193](https://github.com/expo/eas-cli/pull/1193) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [0.54.1](https://github.com/expo/eas-cli/releases/tag/v0.54.1) - 2022-06-15
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add App Store Connect link after `eas metadata:push`. ([#1168](https://github.com/expo/eas-cli/pull/1168) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Surface ASC errors in metadata for normal API rejections. ([#1167](https://github.com/expo/eas-cli/pull/1167) by [@byCedric](https://github.com/byCedric))
|
||||
- Add metadata schema to bundled eas-cli package. ([#1166](https://github.com/expo/eas-cli/pull/1166) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [0.54.0](https://github.com/expo/eas-cli/releases/tag/v0.54.0) - 2022-06-15
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Remove fallback for legacy format in `eas.json`. ([#1158](https://github.com/expo/eas-cli/pull/1158), [#1163](https://github.com/expo/eas-cli/pull/1163) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136](https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00/))
|
||||
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00/))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Update `eas-build-job`. ([#1162](https://github.com/expo/eas-cli/pull/1162) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [0.53.1](https://github.com/expo/eas-cli/releases/tag/v0.53.1) - 2022-06-07
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00/))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Display project archive docs before compressing/uploading to EAS Build. ([#1127](https://github.com/expo/eas-cli/pull/1127) by [@dsokal](https://github.com/dsokal))
|
||||
- Add new common error code for submits. ([#1129](https://github.com/expo/eas-cli/pull/1129) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [0.53.0](https://github.com/expo/eas-cli/releases/tag/v0.53.0) - 2022-05-30
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- Drop support for Node < 14. ([#1098](https://github.com/expo/eas-cli/pull/1098) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Match bundle identifier capabilities more accurately. ([#1112](https://github.com/expo/eas-cli/pull/1112) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- Limit concurrent asset uploads. ([#1153](https://github.com/expo/eas-cli/pull/1153) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
* @dsokal @wkozyra95
|
||||
|
||||
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/schema @bycedric
|
||||
packages/eas-cli/src/metadata @bycedric
|
||||
packages/eas-cli/src/commands/metadata @bycedric
|
||||
|
||||
packages/eas-json @dsokal @wkozyra95
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.53.0"
|
||||
"version": "0.55.1"
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
"oclif": "3.0.1",
|
||||
"prettier": "2.6.2",
|
||||
"ts-jest": "28.0.2",
|
||||
"typescript": "4.6.4"
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.2.0",
|
||||
|
||||
+132
-94
@@ -22,66 +22,69 @@ yarn global add eas-cli
|
||||
# Usage
|
||||
|
||||
```sh
|
||||
eas COMMAND
|
||||
# runs the command
|
||||
eas (-v|--version|version)
|
||||
eas COMMAND
|
||||
# prints the version
|
||||
eas --help COMMAND
|
||||
eas (-v|--version|version)
|
||||
# outputs help for specific command
|
||||
eas --help COMMAND
|
||||
```
|
||||
|
||||
# Commands
|
||||
|
||||
<!-- commands -->
|
||||
* [`eas account:login`](#eas-accountlogin)
|
||||
* [`eas account:logout`](#eas-accountlogout)
|
||||
* [`eas account:view`](#eas-accountview)
|
||||
* [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
|
||||
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
|
||||
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
|
||||
* [`eas branch:list`](#eas-branchlist)
|
||||
* [`eas branch:rename`](#eas-branchrename)
|
||||
* [`eas branch:view [NAME]`](#eas-branchview-name)
|
||||
* [`eas build`](#eas-build)
|
||||
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
* [`eas build:configure`](#eas-buildconfigure)
|
||||
* [`eas build:inspect`](#eas-buildinspect)
|
||||
* [`eas build:list`](#eas-buildlist)
|
||||
* [`eas build:submit`](#eas-buildsubmit)
|
||||
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
* [`eas channel:list`](#eas-channellist)
|
||||
* [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
* [`eas device:create`](#eas-devicecreate)
|
||||
* [`eas device:delete`](#eas-devicedelete)
|
||||
* [`eas device:list`](#eas-devicelist)
|
||||
* [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
* [`eas diagnostics`](#eas-diagnostics)
|
||||
* [`eas help [COMMAND]`](#eas-help-command)
|
||||
* [`eas init`](#eas-init)
|
||||
* [`eas login`](#eas-login)
|
||||
* [`eas logout`](#eas-logout)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas update`](#eas-update)
|
||||
* [`eas update:configure`](#eas-updateconfigure)
|
||||
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
* [`eas update:list`](#eas-updatelist)
|
||||
* [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
* [`eas webhook:create`](#eas-webhookcreate)
|
||||
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
* [`eas webhook:list`](#eas-webhooklist)
|
||||
* [`eas webhook:update`](#eas-webhookupdate)
|
||||
* [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
* [`eas whoami`](#eas-whoami)
|
||||
|
||||
- [`eas account:login`](#eas-accountlogin)
|
||||
- [`eas account:logout`](#eas-accountlogout)
|
||||
- [`eas account:view`](#eas-accountview)
|
||||
- [`eas analytics [STATUS]`](#eas-analytics-status)
|
||||
- [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
|
||||
- [`eas branch:create [NAME]`](#eas-branchcreate-name)
|
||||
- [`eas branch:delete [NAME]`](#eas-branchdelete-name)
|
||||
- [`eas branch:list`](#eas-branchlist)
|
||||
- [`eas branch:rename`](#eas-branchrename)
|
||||
- [`eas branch:view [NAME]`](#eas-branchview-name)
|
||||
- [`eas build`](#eas-build)
|
||||
- [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
|
||||
- [`eas build:configure`](#eas-buildconfigure)
|
||||
- [`eas build:inspect`](#eas-buildinspect)
|
||||
- [`eas build:list`](#eas-buildlist)
|
||||
- [`eas build:submit`](#eas-buildsubmit)
|
||||
- [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
|
||||
- [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
- [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
- [`eas channel:list`](#eas-channellist)
|
||||
- [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
- [`eas config`](#eas-config)
|
||||
- [`eas credentials`](#eas-credentials)
|
||||
- [`eas device:create`](#eas-devicecreate)
|
||||
- [`eas device:delete`](#eas-devicedelete)
|
||||
- [`eas device:list`](#eas-devicelist)
|
||||
- [`eas device:view [UDID]`](#eas-deviceview-udid)
|
||||
- [`eas diagnostics`](#eas-diagnostics)
|
||||
- [`eas help [COMMAND]`](#eas-help-command)
|
||||
- [`eas init`](#eas-init)
|
||||
- [`eas login`](#eas-login)
|
||||
- [`eas logout`](#eas-logout)
|
||||
- [`eas metadata:pull`](#eas-metadatapull)
|
||||
- [`eas metadata:push`](#eas-metadatapush)
|
||||
- [`eas project:info`](#eas-projectinfo)
|
||||
- [`eas project:init`](#eas-projectinit)
|
||||
- [`eas secret:create`](#eas-secretcreate)
|
||||
- [`eas secret:delete`](#eas-secretdelete)
|
||||
- [`eas secret:list`](#eas-secretlist)
|
||||
- [`eas submit`](#eas-submit)
|
||||
- [`eas update`](#eas-update)
|
||||
- [`eas update:configure`](#eas-updateconfigure)
|
||||
- [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
|
||||
- [`eas update:list`](#eas-updatelist)
|
||||
- [`eas update:view GROUPID`](#eas-updateview-groupid)
|
||||
- [`eas webhook:create`](#eas-webhookcreate)
|
||||
- [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
|
||||
- [`eas webhook:list`](#eas-webhooklist)
|
||||
- [`eas webhook:update`](#eas-webhookupdate)
|
||||
- [`eas webhook:view ID`](#eas-webhookview-id)
|
||||
- [`eas whoami`](#eas-whoami)
|
||||
|
||||
## `eas account:login`
|
||||
|
||||
@@ -98,7 +101,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -115,7 +118,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -132,7 +135,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -146,7 +149,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -195,7 +198,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -215,7 +218,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -232,7 +235,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -251,7 +254,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -271,7 +274,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -300,7 +303,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -314,7 +317,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -331,7 +334,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -366,7 +369,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.53.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -399,7 +402,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -444,7 +447,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -464,7 +467,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -485,7 +488,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -502,7 +505,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -522,7 +525,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -540,7 +543,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -554,7 +557,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -568,7 +571,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.53.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -586,7 +589,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -603,7 +606,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -617,7 +620,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -631,7 +634,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -698,6 +701,40 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
generate the local store configuration from the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:pull [--profile <value>]
|
||||
|
||||
FLAGS
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
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.55.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
sync the local store configuration to the app stores
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas metadata:push [--profile <value>]
|
||||
|
||||
FLAGS
|
||||
--profile=<value> Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.
|
||||
|
||||
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.55.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
information about the current project
|
||||
@@ -710,7 +747,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -727,7 +764,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -747,7 +784,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.53.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -764,7 +801,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -778,7 +815,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.53.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -808,7 +845,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -838,7 +875,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -855,7 +892,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -875,7 +912,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.53.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -894,7 +931,7 @@ DESCRIPTION
|
||||
view the recent updates for a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -914,7 +951,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -934,7 +971,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -951,7 +988,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -968,7 +1005,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -989,7 +1026,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1006,7 +1043,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.53.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v0.55.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -1022,4 +1059,5 @@ DESCRIPTION
|
||||
ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
<!-- commandsstop -->
|
||||
Generated
+222
-1
@@ -10523,6 +10523,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "canRetry",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "cancelingActor",
|
||||
"description": null,
|
||||
@@ -10548,7 +10564,7 @@
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"name": "completedAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
@@ -10559,6 +10575,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "createdAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "distribution",
|
||||
"description": null,
|
||||
@@ -10571,6 +10603,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enqueuedAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"description": null,
|
||||
@@ -10731,6 +10775,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "parentBuild",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Build",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "platform",
|
||||
"description": null,
|
||||
@@ -10779,6 +10835,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "provisioningStartedAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "queuePosition",
|
||||
"description": "Queue position is 1-indexed",
|
||||
@@ -10815,6 +10883,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "resourceClass",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "ENUM",
|
||||
"name": "BuildResourceClass",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "runtimeVersion",
|
||||
"description": null,
|
||||
@@ -10883,6 +10963,22 @@
|
||||
"name": "updatedAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "workerStartedAt",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "DateTime",
|
||||
@@ -12196,6 +12292,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "buildParams",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildParamsInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "job",
|
||||
"description": null,
|
||||
@@ -12257,6 +12365,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "buildParams",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildParamsInput",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "job",
|
||||
"description": null,
|
||||
@@ -12330,6 +12450,39 @@
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "retryBuild",
|
||||
"description": "Retry an EAS Build build",
|
||||
"args": [
|
||||
{
|
||||
"name": "buildId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Build",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
@@ -12375,6 +12528,33 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "BuildParamsInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "resourceClass",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "BuildResourceClass",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "BuildPriority",
|
||||
@@ -12751,6 +12931,47 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "BuildResourceClass",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "ANDROID_DEFAULT",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ANDROID_LARGE",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IOS_DEFAULT",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IOS_LARGE",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "LEGACY",
|
||||
"description": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "BuildStatus",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "0.53.0",
|
||||
"version": "0.55.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -13,8 +13,8 @@
|
||||
"@expo/config": "6.0.23",
|
||||
"@expo/config-plugins": "4.1.4",
|
||||
"@expo/config-types": "45.0.0",
|
||||
"@expo/eas-build-job": "0.2.72",
|
||||
"@expo/eas-json": "0.53.0",
|
||||
"@expo/eas-build-job": "0.2.77",
|
||||
"@expo/eas-json": "0.55.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
@@ -30,6 +30,7 @@
|
||||
"@oclif/core": "1.9.0",
|
||||
"@urql/core": "2.4.4",
|
||||
"@urql/exchange-retry": "0.3.3",
|
||||
"ajv": "^6.12.6",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.11.0",
|
||||
"cli-table3": "0.6.2",
|
||||
@@ -60,6 +61,7 @@
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
@@ -101,7 +103,7 @@
|
||||
"nock": "13.2.4",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "2.0.1",
|
||||
"typescript": "4.6.4"
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -109,7 +111,8 @@
|
||||
"files": [
|
||||
"/bin",
|
||||
"/build",
|
||||
"/oclif.manifest.json"
|
||||
"/oclif.manifest.json",
|
||||
"/schema"
|
||||
],
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"keywords": [
|
||||
@@ -142,6 +145,9 @@
|
||||
"device": {
|
||||
"description": "manage Apple devices for Internal Distribution"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "manage store configuration"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage project"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,988 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema",
|
||||
"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",
|
||||
"SIX_TO_EIGHT",
|
||||
"NINE_TO_ELEVEN"
|
||||
]
|
||||
},
|
||||
"AppleRating": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"INFREQUENT_OR_MILD",
|
||||
"FREQUENT_OR_INTENSE"
|
||||
]
|
||||
},
|
||||
"AppleCategory": {
|
||||
"type": "array",
|
||||
"description": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array.",
|
||||
"markdownDescription": "App Store categories for the app, can add up to two categories. The first item in this list is the primary category, the second is the secondary and optional category. `STICKERS` and `GAMES` categories can add up to two additional subcategories, written as nested array. E.g.\n\n- `[\"SPORTS\"]`\n- `[\"SPORTS\", \"LIFESTYLE\"]`\n- `[[\"GAMES\", \"GAMES_CARD\"], \"ENTERTAINMENT\"]`\n- `[\"GAMES\", \"WEATHER\"]`",
|
||||
"minItems": 1,
|
||||
"maxItems": 2,
|
||||
"uniqueItems": true,
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"label": "Basic categories",
|
||||
"description": "Add two simple categories",
|
||||
"body": [
|
||||
"$1",
|
||||
"$2"
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Category with subcategories",
|
||||
"description": "Add a category containing subcategories",
|
||||
"body": [
|
||||
[
|
||||
"${1|GAMES,STICKERS|}",
|
||||
"$3",
|
||||
"$4"
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/root"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/parent"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "The GAMES category can have additional subcategories",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"uniqueItems": true,
|
||||
"items": [
|
||||
{
|
||||
"const": "GAMES"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/games"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/games"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"description": "The STICKERS category can have additional subcategories",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"uniqueItems": true,
|
||||
"items": [
|
||||
{
|
||||
"const": "STICKERS"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleCategory/categories/stickers"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"categories": {
|
||||
"root": {
|
||||
"enum": [
|
||||
"BUSINESS",
|
||||
"SOCIAL_NETWORKING",
|
||||
"MEDICAL",
|
||||
"FOOD_AND_DRINK",
|
||||
"EDUCATION",
|
||||
"BOOKS",
|
||||
"SPORTS",
|
||||
"FINANCE",
|
||||
"REFERENCE",
|
||||
"GRAPHICS_AND_DESIGN",
|
||||
"DEVELOPER_TOOLS",
|
||||
"HEALTH_AND_FITNESS",
|
||||
"MUSIC",
|
||||
"WEATHER",
|
||||
"TRAVEL",
|
||||
"ENTERTAINMENT",
|
||||
"LIFESTYLE",
|
||||
"MAGAZINES_AND_NEWSPAPERS",
|
||||
"UTILITIES",
|
||||
"SHOPPING",
|
||||
"PRODUCTIVITY",
|
||||
"NEWS",
|
||||
"PHOTO_AND_VIDEO",
|
||||
"NAVIGATION"
|
||||
]
|
||||
},
|
||||
"parent": {
|
||||
"enum": [
|
||||
"GAMES",
|
||||
"STICKERS"
|
||||
]
|
||||
},
|
||||
"games": {
|
||||
"enum": [
|
||||
"GAMES_SPORTS",
|
||||
"GAMES_WORD",
|
||||
"GAMES_MUSIC",
|
||||
"GAMES_ADVENTURE",
|
||||
"GAMES_ACTION",
|
||||
"GAMES_ROLE_PLAYING",
|
||||
"GAMES_CASUAL",
|
||||
"GAMES_BOARD",
|
||||
"GAMES_TRIVIA",
|
||||
"GAMES_CARD",
|
||||
"GAMES_PUZZLE",
|
||||
"GAMES_CASINO",
|
||||
"GAMES_STRATEGY",
|
||||
"GAMES_SIMULATION",
|
||||
"GAMES_RACING",
|
||||
"GAMES_FAMILY"
|
||||
]
|
||||
},
|
||||
"stickers": {
|
||||
"enum": [
|
||||
"STICKERS_PLACES_AND_OBJECTS",
|
||||
"STICKERS_EMOJI_AND_EXPRESSIONS",
|
||||
"STICKERS_CELEBRATIONS",
|
||||
"STICKERS_CELEBRITIES",
|
||||
"STICKERS_MOVIES_AND_TV",
|
||||
"STICKERS_SPORTS_AND_ACTIVITIES",
|
||||
"STICKERS_EATING_AND_DRINKING",
|
||||
"STICKERS_CHARACTERS",
|
||||
"STICKERS_ANIMALS",
|
||||
"STICKERS_FASHION",
|
||||
"STICKERS_ART",
|
||||
"STICKERS_GAMING",
|
||||
"STICKERS_KIDS_AND_FAMILY",
|
||||
"STICKERS_PEOPLE",
|
||||
"STICKERS_MUSIC"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleInfo": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"title",
|
||||
"privacyPolicyUrl"
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"label": "Basic",
|
||||
"description": "Create the minimum required locale properties",
|
||||
"body": {
|
||||
"title": "${1:App title}",
|
||||
"privacyPolicyUrl": "https://${3:www.}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Full",
|
||||
"description": "Create all available locale properties",
|
||||
"body": {
|
||||
"title": "${1:App title}",
|
||||
"subtitle": "${2:Subtitle for your app}",
|
||||
"description": "${3:A longer description of what your app does}",
|
||||
"keywords": [
|
||||
"${4:keyword}"
|
||||
],
|
||||
"releaseNotes": "${5:Bug fixes and improved stability}",
|
||||
"promoText": "${6:Short tagline for your app}",
|
||||
"marketingUrl": "https://${7:www.}",
|
||||
"supportUrl": "https://${8:www.}",
|
||||
"privacyPolicyUrl": "https://${9:www.}",
|
||||
"privacyChoicesUrl": "https://${10:www.}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Name of the app in the store. This should be similar to the installed app name.",
|
||||
"minLength": 2,
|
||||
"maxLength": 30,
|
||||
"meta": {
|
||||
"storeInfo": "The name will be reviewed before it is made available on the App Store."
|
||||
}
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"description": "Subtext for the app. Example: 'A Fun Game For Friends'",
|
||||
"maxLength": 30,
|
||||
"meta": {
|
||||
"storeInfo": "The subtitle will be reviewed before it is made available on the App Store."
|
||||
}
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Main description of what the app does",
|
||||
"minLength": 10,
|
||||
"maxLength": 4000,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true
|
||||
}
|
||||
},
|
||||
"keywords": {
|
||||
"type": "array",
|
||||
"description": "List of keywords to help users find the app in the App Store",
|
||||
"uniqueItems": true,
|
||||
"items": {
|
||||
"maxLength": 100,
|
||||
"type": "string"
|
||||
},
|
||||
"meta": {
|
||||
"versioned": true
|
||||
}
|
||||
},
|
||||
"releaseNotes": {
|
||||
"type": "string",
|
||||
"description": "Changes since the last public version",
|
||||
"maxLength": 4000,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true
|
||||
}
|
||||
},
|
||||
"promoText": {
|
||||
"type": "string",
|
||||
"description": "Short tagline for the app",
|
||||
"maxLength": 170,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true,
|
||||
"aso": false
|
||||
}
|
||||
},
|
||||
"marketingUrl": {
|
||||
"type": "string",
|
||||
"description": "URL to the app marketing page",
|
||||
"maxLength": 255,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true
|
||||
}
|
||||
},
|
||||
"supportUrl": {
|
||||
"type": "string",
|
||||
"description": "URL to the app support page",
|
||||
"maxLength": 255,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true
|
||||
}
|
||||
},
|
||||
"privacyPolicyText": {
|
||||
"type": "string",
|
||||
"description": "Privacy policy for Apple TV"
|
||||
},
|
||||
"privacyPolicyUrl": {
|
||||
"type": "string",
|
||||
"description": "A URL that links to your privacy policy. A privacy policy is required for all apps.",
|
||||
"maxLength": 255,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true
|
||||
}
|
||||
},
|
||||
"privacyChoicesUrl": {
|
||||
"type": "string",
|
||||
"description": "A URL where users can modify and delete the data collected from the app, or decide how their data is used and shared.",
|
||||
"maxLength": 255,
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true,
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAdvisory": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"alcoholTobaccoOrDrugUseOrReferences",
|
||||
"contests",
|
||||
"gamblingSimulated",
|
||||
"horrorOrFearThemes",
|
||||
"matureOrSuggestiveThemes",
|
||||
"medicalOrTreatmentInformation",
|
||||
"profanityOrCrudeHumor",
|
||||
"sexualContentGraphicAndNudity",
|
||||
"sexualContentOrNudity",
|
||||
"violenceCartoonOrFantasy",
|
||||
"violenceRealistic",
|
||||
"violenceRealisticProlongedGraphicOrSadistic",
|
||||
"gambling",
|
||||
"unrestrictedWebAccess",
|
||||
"kidsAgeBand",
|
||||
"seventeenPlus"
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"label": "Basic",
|
||||
"description": "Create an advisory that disables all age-rated properties, for when your app does nothing",
|
||||
"body": {
|
||||
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
|
||||
"contests": "NONE",
|
||||
"gamblingSimulated": "NONE",
|
||||
"horrorOrFearThemes": "NONE",
|
||||
"matureOrSuggestiveThemes": "NONE",
|
||||
"medicalOrTreatmentInformation": "NONE",
|
||||
"profanityOrCrudeHumor": "NONE",
|
||||
"sexualContentGraphicAndNudity": "NONE",
|
||||
"sexualContentOrNudity": "NONE",
|
||||
"violenceCartoonOrFantasy": "NONE",
|
||||
"violenceRealistic": "NONE",
|
||||
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
|
||||
"gambling": false,
|
||||
"unrestrictedWebAccess": false,
|
||||
"kidsAgeBand": null,
|
||||
"seventeenPlus": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Full",
|
||||
"description": "Create an advisory that enables some of the age-rated properties",
|
||||
"body": {
|
||||
"alcoholTobaccoOrDrugUseOrReferences": "${1|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"contests": "${2|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"gamblingSimulated": "${3|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"horrorOrFearThemes": "${4|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"matureOrSuggestiveThemes": "${5|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"medicalOrTreatmentInformation": "${6|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"profanityOrCrudeHumor": "${7|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"sexualContentGraphicAndNudity": "${8|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"sexualContentOrNudity": "${9|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"violenceCartoonOrFantasy": "${10|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"violenceRealistic": "${11|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"violenceRealisticProlongedGraphicOrSadistic": "${12|NONE,INFREQUENT_OR_MILD,FREQUENT_OR_INTENSE|}",
|
||||
"unrestrictedWebAccess": false,
|
||||
"gambling": false,
|
||||
"kidsAgeBand": null,
|
||||
"seventeenPlus": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"alcoholTobaccoOrDrugUseOrReferences": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain alcohol, tobacco, or drug use or references?"
|
||||
},
|
||||
"contests": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain contests?"
|
||||
},
|
||||
"gamblingSimulated": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain simulated gambling?"
|
||||
},
|
||||
"medicalOrTreatmentInformation": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain medical or treatment information?"
|
||||
},
|
||||
"profanityOrCrudeHumor": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain profanity or crude humor?"
|
||||
},
|
||||
"sexualContentGraphicAndNudity": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain graphic sexual content and nudity?"
|
||||
},
|
||||
"sexualContentOrNudity": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain sexual content or nudity?"
|
||||
},
|
||||
"horrorOrFearThemes": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain horror or fear themes?"
|
||||
},
|
||||
"matureOrSuggestiveThemes": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain mature or suggestive themes?"
|
||||
},
|
||||
"violenceCartoonOrFantasy": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain cartoon or fantasy violence?"
|
||||
},
|
||||
"violenceRealisticProlongedGraphicOrSadistic": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain prolonged graphic or sadistic realistic violence?"
|
||||
},
|
||||
"violenceRealistic": {
|
||||
"$ref": "#/definitions/apple/AppleRating",
|
||||
"description": "Does the app contain realistic violence?"
|
||||
},
|
||||
"unrestrictedWebAccess": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain unrestricted web access, such as with an embedded browser?"
|
||||
},
|
||||
"gambling": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain gambling?"
|
||||
},
|
||||
"kidsAgeBand": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/apple/AppleKidsAge"
|
||||
}
|
||||
],
|
||||
"description": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n@see https://developer.apple.com/news/?id=091202019a",
|
||||
"markdownDescription": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n\n[Learn more](https://developer.apple.com/news/?id=091202019a)"
|
||||
},
|
||||
"seventeenPlus": {
|
||||
"type": "boolean",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleRelease": {
|
||||
"type": "object",
|
||||
"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": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
},
|
||||
"isPhasedReleaseEnabled": {
|
||||
"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`."
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleReview": {
|
||||
"type": "object",
|
||||
"description": "Info provided to the App Store review team to help them understand how to use your app.",
|
||||
"required": [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phone"
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"label": "Basic",
|
||||
"description": "Only define the contact details in case communcation is needed with the App Store review team.",
|
||||
"body": {
|
||||
"firstName": "$1",
|
||||
"lastName": "$2",
|
||||
"email": "$3",
|
||||
"phone": "+$4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Full",
|
||||
"description": "Define the demo account credentials, additional info, and contact details for the App Store review team.",
|
||||
"body": {
|
||||
"firstName": "$1",
|
||||
"lastName": "$2",
|
||||
"email": "$3",
|
||||
"phone": "+$4",
|
||||
"demoUsername": "$5",
|
||||
"demoPassword": "$6",
|
||||
"demoRequired": true,
|
||||
"notes": "$7"
|
||||
}
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string",
|
||||
"description": "First name of contact in case communication is needed with the App Store review team.",
|
||||
"minLength": 1
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string",
|
||||
"description": "Last name of contact in case communication is needed with the App Store review team.",
|
||||
"minLength": 1
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of contact in case communication is needed with the App Store review team.",
|
||||
"format": "email"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"description": "Phone number of contact in case communication is needed with the App Store review team. Preface the phone number with ‘+’ followed by the country code (for example, +44 844 209 0611)"
|
||||
},
|
||||
"demoUsername": {
|
||||
"type": "string",
|
||||
"description": "The user name to sign in to your app to review its features."
|
||||
},
|
||||
"demoPassword": {
|
||||
"type": "string",
|
||||
"description": "The password to sign in to your app to review its features."
|
||||
},
|
||||
"demoRequired": {
|
||||
"type": "boolean",
|
||||
"description": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review. Credentials must be valid and active for duration of review.",
|
||||
"markdownDescription": "A Boolean value that indicates if sign-in information is required to review all the features of your app. If users sign in using social media, provide information for an account for review.\n\n**Credentials must be valid and active for duration of review.**"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Additional information about your app that can help during the review process. Do not include demo account details.",
|
||||
"markdownDescription": "Additional information about your app that can help during the review process.\n\n**Do not include demo account details.**",
|
||||
"minLength": 2,
|
||||
"maxLength": 4000
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleConfig": {
|
||||
"type": "object",
|
||||
"description": "Configuration that is specific to the Apple App Store.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"advisory": {
|
||||
"$ref": "#/definitions/apple/AppleAdvisory"
|
||||
},
|
||||
"categories": {
|
||||
"$ref": "#/definitions/apple/AppleCategory"
|
||||
},
|
||||
"copyright": {
|
||||
"type": "string",
|
||||
"description": "The updated company copyright. Example: 2021 Evan Bacon",
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"body": "$CURRENT_YEAR ${1:name}"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"versioned": true,
|
||||
"liveEdits": true,
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"description": "Localized core app info",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"ar-SA": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Arabic"
|
||||
},
|
||||
"ca": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Catalan"
|
||||
},
|
||||
"zh-Hans": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Chinese (Simplified)"
|
||||
},
|
||||
"zh-Hant": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Chinese (Traditional)"
|
||||
},
|
||||
"hr": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Croatian"
|
||||
},
|
||||
"cs": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Czech"
|
||||
},
|
||||
"da": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Danish"
|
||||
},
|
||||
"nl-NL": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Dutch"
|
||||
},
|
||||
"en-AU": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "English (Australia)"
|
||||
},
|
||||
"en-CA": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "English (Canada)"
|
||||
},
|
||||
"en-GB": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "English (U.K.)"
|
||||
},
|
||||
"en-US": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "English (U.S.)"
|
||||
},
|
||||
"fi": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Finnish"
|
||||
},
|
||||
"fr-FR": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "French"
|
||||
},
|
||||
"fr-CA": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "French (Canada)"
|
||||
},
|
||||
"de-DE": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "German"
|
||||
},
|
||||
"el": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Greek"
|
||||
},
|
||||
"he": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Hebrew"
|
||||
},
|
||||
"hi": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Hindi"
|
||||
},
|
||||
"hu": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Hungarian"
|
||||
},
|
||||
"id": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Indonesian"
|
||||
},
|
||||
"it": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Italian"
|
||||
},
|
||||
"ja": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Japanese"
|
||||
},
|
||||
"ko": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Korean"
|
||||
},
|
||||
"ms": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Malay"
|
||||
},
|
||||
"no": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Norwegian"
|
||||
},
|
||||
"pl": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Polish"
|
||||
},
|
||||
"pt-BR": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Portuguese (Brazil)"
|
||||
},
|
||||
"pt-PT": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Portuguese (Portugal)"
|
||||
},
|
||||
"ro": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Romanian"
|
||||
},
|
||||
"ru": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Russian"
|
||||
},
|
||||
"sk": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Slovak"
|
||||
},
|
||||
"es-MX": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Spanish (Mexico)"
|
||||
},
|
||||
"es-ES": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Spanish (Spain)"
|
||||
},
|
||||
"sv": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Swedish"
|
||||
},
|
||||
"th": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Thai"
|
||||
},
|
||||
"tr": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Turkish"
|
||||
},
|
||||
"uk": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Ukrainian"
|
||||
},
|
||||
"vi": {
|
||||
"$ref": "#/definitions/apple/AppleInfo",
|
||||
"description": "Vietnamese"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/apple/AppleReview",
|
||||
"description": "Info for the App Store reviewer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"const": 0,
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { fixS3Url } from '../uploads';
|
||||
|
||||
describe(fixS3Url, () => {
|
||||
it('fixes broken links', () => {
|
||||
const input =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
const expectedOutput =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(input)).toBe(expectedOutput);
|
||||
});
|
||||
|
||||
it('does not change correct urls', () => {
|
||||
const correctUrl =
|
||||
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||
expect(fixS3Url(correctUrl)).toBe(correctUrl);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { logEvent } from './rudderstackClient';
|
||||
export type Event = BuildEvent | SubmissionEvent;
|
||||
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
@@ -45,6 +45,11 @@ export enum BuildEvent {
|
||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||
}
|
||||
|
||||
export enum MetadataEvent {
|
||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||
}
|
||||
|
||||
export class Analytics {
|
||||
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||
logEvent(name, properties);
|
||||
|
||||
@@ -5,6 +5,7 @@ import nullthrows from 'nullthrows';
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
@@ -87,7 +88,8 @@ export async function prepareAndroidBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
@@ -95,6 +97,7 @@ export async function prepareAndroidBuildAsync(
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildParamsInput,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
@@ -53,11 +54,22 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
sendBuildRequestAsync(
|
||||
appId: string,
|
||||
job: TJob,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
|
||||
return {
|
||||
resourceClass: ctx.resourceClass,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
@@ -100,6 +112,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const buildParams = resolveBuildParamsInput(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
@@ -111,7 +124,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
@@ -157,6 +170,12 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`Compressing project files and uploading to EAS Build. ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`
|
||||
);
|
||||
const projectTarball = await makeProjectTarballAsync();
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
@@ -169,10 +188,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) =>
|
||||
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
@@ -194,7 +210,8 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
metadata: Metadata
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
@@ -206,7 +223,8 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata
|
||||
metadata,
|
||||
buildParams
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { XCBuildConfiguration } from 'xcode';
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { Target } from '../credentials/ios/types';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||
import { Actor } from '../user/User';
|
||||
@@ -30,6 +31,7 @@ export interface BuildContext<T extends Platform> {
|
||||
accountName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { TrackingContext } from '../analytics/common';
|
||||
import { Analytics, BuildEvent } from '../analytics/events';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
@@ -25,6 +26,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
resourceClass,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -33,6 +35,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
@@ -70,6 +73,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
resourceClass,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
@@ -82,7 +83,8 @@ export async function prepareIosBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
@@ -90,6 +92,7 @@ export async function prepareIosBuildAsync(
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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.82';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.85';
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, SubmitProfile } from '@expo/eas-json';
|
||||
import { BuildProfile, EasJsonReader, SubmitProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildResourceClass,
|
||||
BuildStatus,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
@@ -30,6 +36,7 @@ import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { UserInputResourceClass } from './types';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
@@ -47,8 +54,23 @@ export interface BuildFlags {
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
userInputResourceClass?: UserInputResourceClass;
|
||||
}
|
||||
|
||||
const platformToGraphQLResourceClassMapping: Record<
|
||||
Platform,
|
||||
Record<UserInputResourceClass, BuildResourceClass>
|
||||
> = {
|
||||
[Platform.ANDROID]: {
|
||||
[UserInputResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||
[UserInputResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||
},
|
||||
[Platform.IOS]: {
|
||||
[UserInputResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||
[UserInputResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||
},
|
||||
};
|
||||
|
||||
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
@@ -57,11 +79,12 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
projectDir,
|
||||
easJsonReader,
|
||||
platforms,
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
@@ -84,6 +107,10 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
resourceClass:
|
||||
platformToGraphQLResourceClassMapping[buildProfile.platform][
|
||||
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
|
||||
],
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
@@ -102,7 +129,7 @@ export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFla
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
projectDir,
|
||||
easJsonReader,
|
||||
platforms,
|
||||
profileName: flags.submitProfile,
|
||||
type: 'submit',
|
||||
@@ -160,14 +187,17 @@ async function prepareAndStartBuildAsync({
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
resourceClass,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
resourceClass: BuildResourceClass;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
resourceClass,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
|
||||
@@ -12,3 +12,8 @@ export enum BuildDistributionType {
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export enum UserInputResourceClass {
|
||||
DEFAULT = 'default',
|
||||
LARGE = 'large',
|
||||
}
|
||||
@@ -35,3 +35,16 @@ export function getInternalDistributionInstallUrl(build: BuildFragment): string
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
|
||||
export function getUpdateGroupUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
updateGroupId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/updates/${encodeURIComponent(updateGroupId)}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -1,21 +1,13 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { ensureRepoIsCleanAsync, reviewAndCommitChangesAsync } from '../../build/utils/repository';
|
||||
import { UserInputResourceClass } from '../../build/types';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import Log from '../../log';
|
||||
import { RequestedPlatform, selectRequestedPlatformAsync } from '../../platform';
|
||||
import { findProjectRootAsync } from '../../project/projectUtils';
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { getVcsClient, setVcsClient } from '../../vcs';
|
||||
import GitClient from '../../vcs/clients/git';
|
||||
|
||||
interface RawBuildFlags {
|
||||
platform?: string;
|
||||
@@ -30,6 +22,7 @@ interface RawBuildFlags {
|
||||
json: boolean;
|
||||
'auto-submit': boolean;
|
||||
'auto-submit-with-profile'?: string;
|
||||
'resource-class'?: UserInputResourceClass;
|
||||
}
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
@@ -88,6 +81,11 @@ export default class Build extends EasCommand {
|
||||
helpValue: 'PROFILE_NAME',
|
||||
exclusive: ['auto-submit'],
|
||||
}),
|
||||
'resource-class': Flags.enum({
|
||||
options: Object.values(UserInputResourceClass),
|
||||
hidden: true,
|
||||
description: 'The instance type that will be used to run this build [experimental]',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
@@ -98,7 +96,6 @@ export default class Build extends EasCommand {
|
||||
const flags = await this.sanitizeFlagsAsync(rawFlags);
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await runBuildAndSubmitAsync(projectDir, flags);
|
||||
}
|
||||
@@ -165,70 +162,7 @@ export default class Build extends EasCommand {
|
||||
json: flags['json'],
|
||||
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
userInputResourceClass: flags['resource-class'] ?? UserInputResourceClass.DEFAULT,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleDeprecatedEasJsonAsync(
|
||||
projectDir: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
|
||||
if (!(await fs.pathExists(easJsonPath))) {
|
||||
return;
|
||||
}
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const rawEasJson = await easJsonReader.readAsync();
|
||||
if (rawEasJson?.cli) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
Log.warn(
|
||||
`${
|
||||
figures.warning
|
||||
} Action required: the default behavior of EAS CLI has changed and your eas.json must be updated to remove ambiguity around which Git integration workflow to use. Refer to ${link(
|
||||
'https://expo.fyi/eas-vcs-workflow'
|
||||
)} for more information.`
|
||||
);
|
||||
Log.warn(
|
||||
'This warning will become an error in an upcoming EAS CLI release. For now, we will proceed with the old default behavior to avoid disruption of your builds.'
|
||||
);
|
||||
setVcsClient(new GitClient());
|
||||
return;
|
||||
}
|
||||
Log.log(
|
||||
`${chalk.bold(
|
||||
'eas-cli@>=0.34.0 no longer requires that you commit changes to Git before starting a build.'
|
||||
)} ${learnMore('https://expo.fyi/eas-vcs-workflow')}`
|
||||
);
|
||||
Log.log(
|
||||
`If you want to continue using the Git integration, you can opt in with ${chalk.bold(
|
||||
'cli.requireCommit'
|
||||
)} in ${chalk.bold('eas.json')} or with the following prompt.`
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
const mode = await selectAsync('Select your preferred Git integration', [
|
||||
{ title: 'Require changes to be committed in Git (old default)', value: 'requireCommit' },
|
||||
{ title: 'Allow builds with dirty Git working tree (new default)', value: 'noCommit' },
|
||||
]);
|
||||
|
||||
if (mode === 'requireCommit') {
|
||||
setVcsClient(new GitClient());
|
||||
await ensureRepoIsCleanAsync(nonInteractive);
|
||||
}
|
||||
|
||||
rawEasJson.cli =
|
||||
mode === 'requireCommit'
|
||||
? { version: `>= ${easCliVersion}`, requireCommit: true }
|
||||
: { version: `>= ${easCliVersion}` };
|
||||
await fs.writeJSON(easJsonPath, rawEasJson, { spaces: 2 });
|
||||
Log.withTick('Updated eas.json');
|
||||
if (mode === 'requireCommit') {
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
await reviewAndCommitChangesAsync('Set cli.requireCommit to true in eas.json', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import { appPlatformEmojis } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { handleDeprecatedEasJsonAsync } from './build';
|
||||
|
||||
export default class Config extends EasCommand {
|
||||
static description = 'display project configuration (app.json + eas.json)';
|
||||
@@ -31,7 +30,6 @@ export default class Config extends EasCommand {
|
||||
};
|
||||
|
||||
const projectDir = await findProjectRootAsync();
|
||||
await handleDeprecatedEasJsonAsync(projectDir, false);
|
||||
|
||||
const reader = new EasJsonReader(projectDir);
|
||||
const profileName =
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { downloadMetadataAsync } from '../../metadata/download';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class MetadataPull extends EasCommand {
|
||||
static description = 'generate the local store configuration from the app stores';
|
||||
|
||||
static flags = {
|
||||
profile: Flags.string({
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
|
||||
const { flags } = await this.parse(MetadataPull);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
await getProjectIdAsync(exp);
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
projectDir,
|
||||
user: await ensureLoggedInAsync(),
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const filePath = await downloadMetadataAsync(metadataCtx);
|
||||
const relativePath = path.relative(process.cwd(), filePath);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Your store configuration 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.
|
||||
- ${learnMore('https://docs.expo.dev/eas-metadata/introduction/')}`);
|
||||
} catch (error: any) {
|
||||
handleMetadataError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { ensureProjectConfiguredAsync } from '../../build/configure';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { createMetadataContextAsync } from '../../metadata/context';
|
||||
import { handleMetadataError } from '../../metadata/errors';
|
||||
import { uploadMetadataAsync } from '../../metadata/upload';
|
||||
import { findProjectRootAsync, getProjectIdAsync } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
|
||||
export default class MetadataPush extends EasCommand {
|
||||
static description = 'sync the local store configuration to the app stores';
|
||||
|
||||
static flags = {
|
||||
profile: Flags.string({
|
||||
description:
|
||||
'Name of the submit profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
}),
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
Log.warn('EAS Metadata is in beta and subject to breaking changes.');
|
||||
|
||||
const { flags } = await this.parse(MetadataPush);
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
await getProjectIdAsync(exp);
|
||||
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
projectDir,
|
||||
user: await ensureLoggedInAsync(),
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const metadataCtx = await createMetadataContextAsync({
|
||||
credentialsCtx,
|
||||
projectDir,
|
||||
exp,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const { appleLink } = await uploadMetadataAsync(metadataCtx);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`🎉 Store configuration is synced with the app stores.
|
||||
|
||||
${learnMore(appleLink, { learnMoreMessage: 'See the changes in App Store Connect' })}`);
|
||||
} catch (error: any) {
|
||||
handleMetadataError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
@@ -95,7 +96,7 @@ export default class Submit extends EasCommand {
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
projectDir,
|
||||
easJsonReader: new EasJsonReader(projectDir),
|
||||
platforms,
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import gql from 'graphql-tag';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import fetch from '../../fetch';
|
||||
import { graphqlClient, withErrorHandlingAsync } from '../../graphql/client';
|
||||
@@ -25,11 +26,12 @@ import {
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getExpoConfig } from '../../project/expoConfig';
|
||||
import {
|
||||
findProjectRootAsync,
|
||||
getProjectAccountName,
|
||||
getProjectIdAsync,
|
||||
installExpoUpdatesAsync,
|
||||
isExpoUpdatesInstalledOrAvailable,
|
||||
@@ -44,6 +46,7 @@ import {
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { confirmAsync, promptAsync, selectAsync } from '../../prompts';
|
||||
import { formatUpdate } from '../../update/utils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
@@ -59,7 +62,7 @@ import { listBranchesAsync } from '../branch/list';
|
||||
import { createUpdateChannelOnAppAsync } from '../channel/create';
|
||||
|
||||
export const defaultPublishPlatforms: PublishPlatform[] = ['android', 'ios'];
|
||||
type PlatformFlag = PublishPlatform | 'all';
|
||||
export type PublishPlatformFlag = PublishPlatform | 'all';
|
||||
|
||||
async function getUpdateGroupAsync({
|
||||
group,
|
||||
@@ -117,12 +120,16 @@ async function ensureChannelExistsAsync({
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureBranchExistsAsync({
|
||||
export async function ensureBranchExistsAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
limit,
|
||||
offset,
|
||||
}: {
|
||||
appId: string;
|
||||
name: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}): Promise<{
|
||||
id: string;
|
||||
updates: Exclude<
|
||||
@@ -133,6 +140,8 @@ async function ensureBranchExistsAsync({
|
||||
const { app } = await UpdateQuery.viewBranchAsync({
|
||||
appId,
|
||||
name: branchName,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
const updateBranch = app?.byId.updateBranchByName;
|
||||
if (updateBranch) {
|
||||
@@ -221,7 +230,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const platformFlag = platform as PlatformFlag;
|
||||
const platformFlag = platform as PublishPlatformFlag;
|
||||
// If a group was specified, that means we are republishing it.
|
||||
republish = group ? true : republish;
|
||||
|
||||
@@ -309,13 +318,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
|
||||
const { id: branchId, updates } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let oldMessage: string, oldRuntimeVersion: string;
|
||||
let uploadedAssetCount = 0;
|
||||
|
||||
if (republish) {
|
||||
// If we are republishing, we don't need to worry about building the bundle or uploading the assets.
|
||||
// Instead we get the `updateInfoGroup` from the update we wish to republish.
|
||||
@@ -335,26 +341,12 @@ export default class UpdatePublish extends EasCommand {
|
||||
throw new Error('You must specify the update group to republish.');
|
||||
}
|
||||
|
||||
const updateGroups = uniqBy(updates, u => u.group)
|
||||
.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return platformFlag === 'all' || update.platform === platformFlag;
|
||||
})
|
||||
.map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (updateGroups.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'which update would you like to republish?',
|
||||
updateGroups
|
||||
updatesToRepublish = await getUpdatesToRepublishInteractiveAsync(
|
||||
projectId,
|
||||
branchName,
|
||||
platformFlag,
|
||||
50
|
||||
);
|
||||
updatesToRepublish = updates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
const updatesToRepublishFilteredByPlatform = updatesToRepublish.filter(
|
||||
// Only republish to the specified platforms
|
||||
@@ -362,7 +354,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
);
|
||||
if (updatesToRepublishFilteredByPlatform.length === 0) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published on the platform(s) "${platformFlag}" with group ID "${
|
||||
`There are no updates on branch "${branchName}" published for the platform(s) "${platformFlag}" with group ID "${
|
||||
group ? group : updatesToRepublish[0].group
|
||||
}". Did you mean to publish a new update instead?`
|
||||
);
|
||||
@@ -449,15 +441,28 @@ export default class UpdatePublish extends EasCommand {
|
||||
try {
|
||||
const platforms = platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
const assets = await collectAssetsAsync({ inputDir: inputDir!, platforms });
|
||||
await uploadAssetsAsync(assets);
|
||||
const { uniqueUploadedAssetCount } = await uploadAssetsAsync(
|
||||
assets,
|
||||
(totalAssets, missingAssets) => {
|
||||
assetSpinner.text = `Uploading assets. Finished (${
|
||||
totalAssets - missingAssets
|
||||
}/${totalAssets})`;
|
||||
}
|
||||
);
|
||||
uploadedAssetCount = uniqueUploadedAssetCount;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
assetSpinner.succeed('Uploaded assets!');
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'asset' : 'assets'}!`
|
||||
: `Uploading assets skipped -- no new assets found!`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload assets');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
const truncatedMessage = truncatePublishUpdateMessage(message!);
|
||||
|
||||
const runtimeToPlatformMapping: Record<string, string[]> = {};
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
runtimeToPlatformMapping[runtime] = Object.entries(runtimeVersions)
|
||||
@@ -465,6 +470,12 @@ export default class UpdatePublish extends EasCommand {
|
||||
.map(pair => pair[0]);
|
||||
}
|
||||
|
||||
const { id: branchId } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
limit: 0,
|
||||
});
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] = Object.entries(runtimeToPlatformMapping).map(
|
||||
([runtime, platforms]) => {
|
||||
@@ -484,7 +495,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion: republish ? oldRuntimeVersion : runtime,
|
||||
message,
|
||||
message: truncatedMessage,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
@@ -554,20 +565,36 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
for (const runtime of new Set(Object.values(runtimeVersions))) {
|
||||
const platforms = newUpdates
|
||||
.filter(update => update.runtimeVersion === runtime)
|
||||
.map(update => update.platform);
|
||||
const newUpdate = newUpdates.find(update => update.runtimeVersion === runtime);
|
||||
if (!newUpdate) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime
|
||||
);
|
||||
if (!newUpdatesForRuntimeVersion.length) {
|
||||
throw new Error(`Publish response is missing updates with runtime ${runtime}.`);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
update => update.platform === 'android'
|
||||
);
|
||||
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'branch', value: branchName },
|
||||
{ label: 'runtime version', value: runtime },
|
||||
{ label: 'platform', value: platforms.join(', ') },
|
||||
{ label: 'update group ID', value: newUpdate.group },
|
||||
{ label: 'message', value: message! },
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: truncatedMessage },
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
@@ -576,9 +603,76 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUpdatesToRepublishInteractiveAsync(
|
||||
projectId: string,
|
||||
branchName: string,
|
||||
platformFlag: string,
|
||||
pageSize: number,
|
||||
offset: number = 0,
|
||||
cumulativeUpdates: Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'] = []
|
||||
): Promise<
|
||||
Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates']
|
||||
> {
|
||||
const fetchMoreValue = '_fetchMore';
|
||||
|
||||
const { updates } = await ensureBranchExistsAsync({
|
||||
appId: projectId,
|
||||
name: branchName,
|
||||
limit: pageSize + 1, // fetch an extra item so we know if there are additional pages
|
||||
offset,
|
||||
});
|
||||
cumulativeUpdates = [
|
||||
...cumulativeUpdates,
|
||||
// drop that extra item used for pagination from our render logic
|
||||
...updates.slice(0, updates.length - 1),
|
||||
];
|
||||
const cumulativeUpdatesForTargetPlatforms =
|
||||
platformFlag === 'all'
|
||||
? cumulativeUpdates
|
||||
: cumulativeUpdates.filter(update => {
|
||||
// Only show groups that have updates on the specified platform(s).
|
||||
return update.platform === platformFlag;
|
||||
});
|
||||
const updateGroups = uniqBy(cumulativeUpdatesForTargetPlatforms, u => u.group).map(update => ({
|
||||
title: formatUpdateTitle(update),
|
||||
value: update.group,
|
||||
}));
|
||||
if (!updateGroups.length) {
|
||||
throw new Error(
|
||||
`There are no updates on branch "${branchName}" published for the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
}
|
||||
|
||||
if (updates.length > pageSize) {
|
||||
updateGroups.push({ title: 'Next page...', value: fetchMoreValue });
|
||||
}
|
||||
|
||||
const selectedUpdateGroup = await selectAsync<string>(
|
||||
'Which update would you like to republish?',
|
||||
updateGroups
|
||||
);
|
||||
if (selectedUpdateGroup === fetchMoreValue) {
|
||||
return await getUpdatesToRepublishInteractiveAsync(
|
||||
projectId,
|
||||
branchName,
|
||||
platformFlag,
|
||||
pageSize,
|
||||
offset + pageSize,
|
||||
cumulativeUpdates
|
||||
);
|
||||
}
|
||||
return cumulativeUpdates.filter(update => update.group === selectedUpdateGroup);
|
||||
}
|
||||
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platformFlag: PlatformFlag,
|
||||
platformFlag: PublishPlatformFlag,
|
||||
projectDir: string
|
||||
): Promise<Record<string, string>> {
|
||||
const platforms = (platformFlag === 'all' ? ['android', 'ios'] : [platformFlag]) as Platform[];
|
||||
@@ -645,3 +739,11 @@ async function checkEASUpdateURLIsSetAsync(exp: ExpoConfig): Promise<void> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const truncatePublishUpdateMessage = (originalMessage: string): string => {
|
||||
if (originalMessage.length > 1024) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
return originalMessage.substring(0, 1021) + '...';
|
||||
}
|
||||
return originalMessage;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
export default {
|
||||
/**
|
||||
* Dangerous overrides, use only if you know what you are doing
|
||||
*/
|
||||
|
||||
/**
|
||||
* Overrides applicationId from Android project, setting this option will also
|
||||
* ignore failures when parsing build.gradle.
|
||||
*/
|
||||
overrideAndroidApplicationId: process.env.EAS_DANGEROUS_OVERRIDE_ANDROID_APPLICATION_ID,
|
||||
};
|
||||
+38
-7
@@ -1564,10 +1564,13 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
appVersion?: Maybe<Scalars['String']>;
|
||||
artifacts?: Maybe<BuildArtifacts>;
|
||||
buildProfile?: Maybe<Scalars['String']>;
|
||||
canRetry: Scalars['Boolean'];
|
||||
cancelingActor?: Maybe<Actor>;
|
||||
channel?: Maybe<Scalars['String']>;
|
||||
createdAt?: Maybe<Scalars['DateTime']>;
|
||||
completedAt?: Maybe<Scalars['DateTime']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
distribution?: Maybe<DistributionType>;
|
||||
enqueuedAt?: Maybe<Scalars['DateTime']>;
|
||||
error?: Maybe<BuildError>;
|
||||
estimatedWaitTimeLeftSeconds?: Maybe<Scalars['Int']>;
|
||||
expirationDate?: Maybe<Scalars['DateTime']>;
|
||||
@@ -1582,18 +1585,22 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
|
||||
isGitWorkingTreeDirty?: Maybe<Scalars['Boolean']>;
|
||||
logFiles: Array<Scalars['String']>;
|
||||
metrics?: Maybe<BuildMetrics>;
|
||||
parentBuild?: Maybe<Build>;
|
||||
platform: AppPlatform;
|
||||
priority: BuildPriority;
|
||||
project: Project;
|
||||
provisioningStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
/** Queue position is 1-indexed */
|
||||
queuePosition?: Maybe<Scalars['Int']>;
|
||||
reactNativeVersion?: Maybe<Scalars['String']>;
|
||||
releaseChannel?: Maybe<Scalars['String']>;
|
||||
resourceClass?: Maybe<BuildResourceClass>;
|
||||
runtimeVersion?: Maybe<Scalars['String']>;
|
||||
sdkVersion?: Maybe<Scalars['String']>;
|
||||
status: BuildStatus;
|
||||
submissions: Array<Submission>;
|
||||
updatedAt?: Maybe<Scalars['DateTime']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
workerStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export type BuildArtifact = {
|
||||
@@ -1765,6 +1772,8 @@ export type BuildMutation = {
|
||||
createIosBuild: CreateBuildResult;
|
||||
/** Delete an EAS Build build */
|
||||
deleteBuild: Build;
|
||||
/** Retry an EAS Build build */
|
||||
retryBuild: Build;
|
||||
};
|
||||
|
||||
|
||||
@@ -1775,6 +1784,7 @@ export type BuildMutationCancelBuildArgs = {
|
||||
|
||||
export type BuildMutationCreateAndroidBuildArgs = {
|
||||
appId: Scalars['ID'];
|
||||
buildParams?: InputMaybe<BuildParamsInput>;
|
||||
job: AndroidJobInput;
|
||||
metadata?: InputMaybe<BuildMetadataInput>;
|
||||
};
|
||||
@@ -1782,6 +1792,7 @@ export type BuildMutationCreateAndroidBuildArgs = {
|
||||
|
||||
export type BuildMutationCreateIosBuildArgs = {
|
||||
appId: Scalars['ID'];
|
||||
buildParams?: InputMaybe<BuildParamsInput>;
|
||||
job: IosJobInput;
|
||||
metadata?: InputMaybe<BuildMetadataInput>;
|
||||
};
|
||||
@@ -1791,10 +1802,19 @@ export type BuildMutationDeleteBuildArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
};
|
||||
|
||||
|
||||
export type BuildMutationRetryBuildArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type BuildOrBuildJob = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type BuildParamsInput = {
|
||||
resourceClass: BuildResourceClass;
|
||||
};
|
||||
|
||||
export enum BuildPriority {
|
||||
High = 'HIGH',
|
||||
Normal = 'NORMAL'
|
||||
@@ -1862,6 +1882,14 @@ export type BuildQueryByIdArgs = {
|
||||
buildId: Scalars['ID'];
|
||||
};
|
||||
|
||||
export enum BuildResourceClass {
|
||||
AndroidDefault = 'ANDROID_DEFAULT',
|
||||
AndroidLarge = 'ANDROID_LARGE',
|
||||
IosDefault = 'IOS_DEFAULT',
|
||||
IosLarge = 'IOS_LARGE',
|
||||
Legacy = 'LEGACY'
|
||||
}
|
||||
|
||||
export enum BuildStatus {
|
||||
Canceled = 'CANCELED',
|
||||
Errored = 'ERRORED',
|
||||
@@ -4306,19 +4334,21 @@ export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
job: AndroidJobInput;
|
||||
metadata?: InputMaybe<BuildMetadataInput>;
|
||||
buildParams?: InputMaybe<BuildParamsInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __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 | null, updatedAt?: any | 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 } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __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 } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type CreateIosBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID'];
|
||||
job: IosJobInput;
|
||||
metadata?: InputMaybe<BuildMetadataInput>;
|
||||
buildParams?: InputMaybe<BuildParamsInput>;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __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 | null, updatedAt?: any | 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 } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __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 } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
@@ -4431,7 +4461,7 @@ 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 | null, updatedAt?: any | 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 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 GetAllBuildsForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -4441,7 +4471,7 @@ export type GetAllBuildsForAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type GetAllBuildsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __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 | null, updatedAt?: any | 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 GetAllBuildsForAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __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 GetChannelByNameForAppQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
@@ -4510,6 +4540,7 @@ export type ViewBranchUpdatesQueryVariables = Exact<{
|
||||
appId: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
limit: Scalars['Int'];
|
||||
offset: Scalars['Int'];
|
||||
}>;
|
||||
|
||||
|
||||
@@ -4537,7 +4568,7 @@ export type WebhookByIdQuery = { __typename?: 'RootQuery', webhook: { __typename
|
||||
|
||||
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string };
|
||||
|
||||
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 | null, updatedAt?: any | 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 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 EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, createdAt: any };
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
AndroidJobInput,
|
||||
BuildFragment,
|
||||
BuildMetadataInput,
|
||||
BuildParamsInput,
|
||||
CreateAndroidBuildMutation,
|
||||
CreateAndroidBuildMutationVariables,
|
||||
CreateIosBuildMutation,
|
||||
@@ -26,6 +27,7 @@ export const BuildMutation = {
|
||||
appId: string;
|
||||
job: AndroidJobInput;
|
||||
metadata: BuildMetadataInput;
|
||||
buildParams: BuildParamsInput;
|
||||
}): Promise<BuildResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -35,9 +37,15 @@ export const BuildMutation = {
|
||||
$appId: ID!
|
||||
$job: AndroidJobInput!
|
||||
$metadata: BuildMetadataInput
|
||||
$buildParams: BuildParamsInput
|
||||
) {
|
||||
build {
|
||||
createAndroidBuild(appId: $appId, job: $job, metadata: $metadata) {
|
||||
createAndroidBuild(
|
||||
appId: $appId
|
||||
job: $job
|
||||
metadata: $metadata
|
||||
buildParams: $buildParams
|
||||
) {
|
||||
build {
|
||||
id
|
||||
...BuildFragment
|
||||
@@ -62,6 +70,7 @@ export const BuildMutation = {
|
||||
appId: string;
|
||||
job: IosJobInput;
|
||||
metadata: BuildMetadataInput;
|
||||
buildParams: BuildParamsInput;
|
||||
}): Promise<BuildResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
@@ -71,9 +80,15 @@ export const BuildMutation = {
|
||||
$appId: ID!
|
||||
$job: IosJobInput!
|
||||
$metadata: BuildMetadataInput
|
||||
$buildParams: BuildParamsInput
|
||||
) {
|
||||
build {
|
||||
createIosBuild(appId: $appId, job: $job, metadata: $metadata) {
|
||||
createIosBuild(
|
||||
appId: $appId
|
||||
job: $job
|
||||
metadata: $metadata
|
||||
buildParams: $buildParams
|
||||
) {
|
||||
build {
|
||||
id
|
||||
...BuildFragment
|
||||
|
||||
@@ -8,7 +8,11 @@ import {
|
||||
ViewBranchUpdatesQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
const PAGE_LIMIT = 10_000;
|
||||
export const viewBranchUpdatesQueryUpdateLimit = 300;
|
||||
|
||||
type ViewBranchUpdatesQueryVariablesWithOptionalLimitAndOffset =
|
||||
Partial<ViewBranchUpdatesQueryVariables> &
|
||||
Pick<ViewBranchUpdatesQueryVariables, 'appId' | 'name'>;
|
||||
|
||||
export const UpdateQuery = {
|
||||
async viewAllAsync({ appId }: { appId: string }): Promise<ViewAllUpdatesQuery> {
|
||||
@@ -47,26 +51,31 @@ export const UpdateQuery = {
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
limit: PAGE_LIMIT,
|
||||
limit: viewBranchUpdatesQueryUpdateLimit,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch', 'Update'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
},
|
||||
async viewBranchAsync({ appId, name }: Pick<ViewBranchUpdatesQueryVariables, 'appId' | 'name'>) {
|
||||
async viewBranchAsync({
|
||||
appId,
|
||||
name,
|
||||
limit = viewBranchUpdatesQueryUpdateLimit,
|
||||
offset = 0,
|
||||
}: ViewBranchUpdatesQueryVariablesWithOptionalLimitAndOffset) {
|
||||
return withErrorHandlingAsync<ViewBranchUpdatesQuery>(
|
||||
graphqlClient
|
||||
.query<ViewBranchUpdatesQuery, ViewBranchUpdatesQueryVariables>(
|
||||
gql`
|
||||
query ViewBranchUpdates($appId: String!, $name: String!, $limit: Int!) {
|
||||
query ViewBranchUpdates($appId: String!, $name: String!, $limit: Int!, $offset: Int!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
updateBranchByName(name: $name) {
|
||||
id
|
||||
name
|
||||
updates(offset: 0, limit: $limit) {
|
||||
updates(offset: $offset, limit: $limit) {
|
||||
id
|
||||
group
|
||||
message
|
||||
@@ -92,7 +101,8 @@ export const UpdateQuery = {
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
limit: PAGE_LIMIT,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch', 'Update'] }
|
||||
)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { AgeRatingDeclaration, KidsAge, Rating } from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../../../utils/asc';
|
||||
|
||||
// @ts-expect-error: Deprecated property `gamblingAndContests` needs to be deleted from the types
|
||||
// These attributes is what we get from the API when no questions are answered
|
||||
export const emptyAdvisory: AttributesOf<AgeRatingDeclaration> = {
|
||||
alcoholTobaccoOrDrugUseOrReferences: null,
|
||||
contests: null,
|
||||
gamblingSimulated: null,
|
||||
horrorOrFearThemes: null,
|
||||
matureOrSuggestiveThemes: null,
|
||||
medicalOrTreatmentInformation: null,
|
||||
profanityOrCrudeHumor: null,
|
||||
sexualContentGraphicAndNudity: null,
|
||||
sexualContentOrNudity: null,
|
||||
violenceCartoonOrFantasy: null,
|
||||
violenceRealistic: null,
|
||||
violenceRealisticProlongedGraphicOrSadistic: null,
|
||||
gambling: false,
|
||||
unrestrictedWebAccess: false,
|
||||
kidsAgeBand: null,
|
||||
seventeenPlus: false,
|
||||
};
|
||||
|
||||
// @ts-expect-error: Deprecated property `gamblingAndContests` needs to be deleted from the types
|
||||
export const leastRestrictiveAdvisory: AttributesOf<AgeRatingDeclaration> = {
|
||||
alcoholTobaccoOrDrugUseOrReferences: Rating.NONE,
|
||||
contests: Rating.NONE,
|
||||
gamblingSimulated: Rating.NONE,
|
||||
horrorOrFearThemes: Rating.NONE,
|
||||
matureOrSuggestiveThemes: Rating.NONE,
|
||||
medicalOrTreatmentInformation: Rating.NONE,
|
||||
profanityOrCrudeHumor: Rating.NONE,
|
||||
sexualContentGraphicAndNudity: Rating.NONE,
|
||||
sexualContentOrNudity: Rating.NONE,
|
||||
violenceCartoonOrFantasy: Rating.NONE,
|
||||
violenceRealistic: Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic: Rating.NONE,
|
||||
gambling: false,
|
||||
unrestrictedWebAccess: false,
|
||||
kidsAgeBand: null,
|
||||
seventeenPlus: false,
|
||||
};
|
||||
|
||||
// @ts-expect-error: Deprecated property `gamblingAndContests` needs to be deleted from the types
|
||||
export const mostRestrictiveAdvisory: AttributesOf<AgeRatingDeclaration> = {
|
||||
alcoholTobaccoOrDrugUseOrReferences: Rating.FREQUENT_OR_INTENSE,
|
||||
contests: Rating.FREQUENT_OR_INTENSE,
|
||||
gamblingSimulated: Rating.FREQUENT_OR_INTENSE,
|
||||
horrorOrFearThemes: Rating.FREQUENT_OR_INTENSE,
|
||||
matureOrSuggestiveThemes: Rating.FREQUENT_OR_INTENSE,
|
||||
medicalOrTreatmentInformation: Rating.FREQUENT_OR_INTENSE,
|
||||
profanityOrCrudeHumor: Rating.FREQUENT_OR_INTENSE,
|
||||
sexualContentGraphicAndNudity: Rating.FREQUENT_OR_INTENSE,
|
||||
sexualContentOrNudity: Rating.FREQUENT_OR_INTENSE,
|
||||
violenceCartoonOrFantasy: Rating.FREQUENT_OR_INTENSE,
|
||||
violenceRealistic: Rating.FREQUENT_OR_INTENSE,
|
||||
violenceRealisticProlongedGraphicOrSadistic: Rating.FREQUENT_OR_INTENSE,
|
||||
gambling: true,
|
||||
unrestrictedWebAccess: true,
|
||||
kidsAgeBand: null,
|
||||
seventeenPlus: true,
|
||||
};
|
||||
|
||||
// @ts-expect-error: Deprecated property `gamblingAndContests` needs to be deleted from the types
|
||||
export const kidsSixToEightAdvisory: AttributesOf<AgeRatingDeclaration> = {
|
||||
alcoholTobaccoOrDrugUseOrReferences: Rating.NONE,
|
||||
contests: Rating.NONE,
|
||||
gamblingSimulated: Rating.NONE,
|
||||
horrorOrFearThemes: Rating.NONE,
|
||||
matureOrSuggestiveThemes: Rating.NONE,
|
||||
medicalOrTreatmentInformation: Rating.NONE,
|
||||
profanityOrCrudeHumor: Rating.NONE,
|
||||
sexualContentGraphicAndNudity: Rating.NONE,
|
||||
sexualContentOrNudity: Rating.NONE,
|
||||
violenceCartoonOrFantasy: Rating.NONE,
|
||||
violenceRealistic: Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic: Rating.NONE,
|
||||
gambling: false,
|
||||
unrestrictedWebAccess: false,
|
||||
kidsAgeBand: KidsAge.SIX_TO_EIGHT,
|
||||
seventeenPlus: false,
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { AppCategory, AppCategoryId, AppSubcategoryId, BundleIdPlatform } from '@expo/apple-utils';
|
||||
|
||||
import { AppleConfigWriter } from '../../writer';
|
||||
|
||||
/** Infered type of the AppleConfigWriter.setCategories attributes */
|
||||
type CategoryInfoProps = Parameters<typeof AppleConfigWriter.prototype.setCategories>[0];
|
||||
|
||||
/**
|
||||
* Export a factory method instead of fixed categories.
|
||||
* In the tests, we need to create a couple of different categories,
|
||||
* adding them here as fixed objects won't be readable.
|
||||
*/
|
||||
export function makeCategory(id?: AppCategoryId | AppSubcategoryId): AppCategory | undefined {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return new AppCategory({} as any, id, {
|
||||
platforms: [BundleIdPlatform.IOS, BundleIdPlatform.MAC_OS],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Export a helper method to create the AppInfo object, using only AppCategoryIds or AppSubcategoryId.
|
||||
*/
|
||||
export function makeCategoryInfo(
|
||||
attributes: Partial<Record<keyof CategoryInfoProps, AppCategoryId | AppSubcategoryId>>
|
||||
): CategoryInfoProps {
|
||||
return {
|
||||
primaryCategory: makeCategory(attributes.primaryCategory),
|
||||
primarySubcategoryOne: makeCategory(attributes.primarySubcategoryOne),
|
||||
primarySubcategoryTwo: makeCategory(attributes.primarySubcategoryTwo),
|
||||
secondaryCategory: makeCategory(attributes.secondaryCategory),
|
||||
secondarySubcategoryOne: makeCategory(attributes.secondarySubcategoryOne),
|
||||
secondarySubcategoryTwo: makeCategory(attributes.secondarySubcategoryTwo),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AppInfoLocalization } from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../../../utils/asc';
|
||||
|
||||
export const englishInfo: AttributesOf<AppInfoLocalization> = {
|
||||
locale: 'en-US',
|
||||
name: 'Awesome test app',
|
||||
subtitle: 'This is just a test',
|
||||
privacyPolicyUrl: 'https://example.com/en/privacy-policy',
|
||||
privacyChoicesUrl: 'https://exmaple.com/en/privacy-choices',
|
||||
privacyPolicyText: 'This is some privacy policy text',
|
||||
};
|
||||
|
||||
export const dutchInfo: AttributesOf<AppInfoLocalization> = {
|
||||
locale: 'nl-NL',
|
||||
name: 'Geweldige test app',
|
||||
subtitle: 'Dit is maar een test',
|
||||
privacyPolicyUrl: 'https://example.com/nl/privacy-policy',
|
||||
privacyChoicesUrl: 'https://example.com/nl/privacy-choices',
|
||||
privacyPolicyText: 'Dit is wat privacy policy tekst',
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AppStoreReviewDetail } from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../../../utils/asc';
|
||||
|
||||
export const nameOnlyReviewDetails: AttributesOf<AppStoreReviewDetail> = {
|
||||
contactFirstName: 'Evan',
|
||||
contactLastName: 'Bacon',
|
||||
contactEmail: '[email protected]',
|
||||
contactPhone: '+1 (555) 555-5555',
|
||||
demoAccountName: null,
|
||||
demoAccountPassword: null,
|
||||
demoAccountRequired: null,
|
||||
notes: null,
|
||||
};
|
||||
|
||||
export const nameAndDemoReviewDetails: AttributesOf<AppStoreReviewDetail> = {
|
||||
contactFirstName: 'Evan',
|
||||
contactLastName: 'Bacon',
|
||||
contactEmail: '[email protected]',
|
||||
contactPhone: '+1 (555) 555-5555',
|
||||
demoAccountName: '[email protected]',
|
||||
demoAccountPassword: 'secretpassword',
|
||||
demoAccountRequired: true,
|
||||
notes: null,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { AppStoreState, AppStoreVersion, Platform, ReleaseType } from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../../../utils/asc';
|
||||
|
||||
export const manualRelease: AttributesOf<AppStoreVersion> = {
|
||||
platform: Platform.IOS,
|
||||
versionString: '1.0.0',
|
||||
appStoreState: AppStoreState.WAITING_FOR_REVIEW,
|
||||
storeIcon: null,
|
||||
watchStoreIcon: null,
|
||||
copyright: '2022 - ACME',
|
||||
releaseType: ReleaseType.MANUAL,
|
||||
earliestReleaseDate: null,
|
||||
usesIdfa: null,
|
||||
isWatchOnly: false,
|
||||
downloadable: false,
|
||||
createdDate: '2022-05-23T00:00:00.000Z',
|
||||
};
|
||||
|
||||
export const automaticRelease: AttributesOf<AppStoreVersion> = {
|
||||
platform: Platform.IOS,
|
||||
versionString: '2.0.0',
|
||||
appStoreState: AppStoreState.WAITING_FOR_REVIEW,
|
||||
storeIcon: null,
|
||||
watchStoreIcon: null,
|
||||
copyright: '2022 - ACME',
|
||||
releaseType: ReleaseType.AFTER_APPROVAL,
|
||||
earliestReleaseDate: null,
|
||||
usesIdfa: null,
|
||||
isWatchOnly: false,
|
||||
downloadable: false,
|
||||
createdDate: '2022-05-23T00:00:00.000Z',
|
||||
};
|
||||
|
||||
export const scheduledRelease: AttributesOf<AppStoreVersion> = {
|
||||
platform: Platform.IOS,
|
||||
versionString: '3.0.0',
|
||||
appStoreState: AppStoreState.READY_FOR_SALE,
|
||||
storeIcon: null,
|
||||
watchStoreIcon: null,
|
||||
copyright: '2022 - ACME',
|
||||
releaseType: ReleaseType.SCHEDULED,
|
||||
earliestReleaseDate: '2022-05-29T00:00:00.000Z',
|
||||
usesIdfa: null,
|
||||
isWatchOnly: false,
|
||||
downloadable: false,
|
||||
createdDate: '2022-05-23T00:00:00.000Z',
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { AppStoreVersionLocalization } from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../../../utils/asc';
|
||||
|
||||
export const englishVersion: AttributesOf<AppStoreVersionLocalization> = {
|
||||
locale: 'en-US',
|
||||
description: 'This is a description of this version',
|
||||
keywords: 'some, description',
|
||||
marketingUrl: 'https://example.com/en/marketing',
|
||||
promotionalText: 'This is some promotional text',
|
||||
supportUrl: 'https://example.com/en/support',
|
||||
whatsNew: 'Bugfixes and improvements',
|
||||
};
|
||||
|
||||
export const dutchVersion: AttributesOf<AppStoreVersionLocalization> = {
|
||||
locale: 'nl-NL',
|
||||
description: 'Dit is een beschrijving van deze versie',
|
||||
keywords: 'een, beschrijving',
|
||||
marketingUrl: 'https://example.com/nl/marketing',
|
||||
promotionalText: 'Dit is wat promotie tekst',
|
||||
supportUrl: 'https://example.com/nl/support',
|
||||
whatsNew: 'Beestreparaties en verbeteringen',
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
import { AppCategoryId, AppSubcategoryId } from '@expo/apple-utils';
|
||||
|
||||
import { AppleConfigReader } from '../reader';
|
||||
import { leastRestrictiveAdvisory, mostRestrictiveAdvisory } from './fixtures/ageRatingDeclaration';
|
||||
|
||||
describe('getCategories', () => {
|
||||
it('ignores categories when not set', () => {
|
||||
const reader = new AppleConfigReader({ categories: undefined });
|
||||
expect(reader.getCategories()).toBeNull();
|
||||
});
|
||||
|
||||
it('returns primary category only', () => {
|
||||
const reader = new AppleConfigReader({ categories: [AppCategoryId.ENTERTAINMENT] });
|
||||
expect(reader.getCategories()).toMatchObject({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns primary and secondary categories', () => {
|
||||
const reader = new AppleConfigReader({
|
||||
categories: [AppCategoryId.ENTERTAINMENT, AppCategoryId.FOOD_AND_DRINK],
|
||||
});
|
||||
expect(reader.getCategories()).toMatchObject({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: AppCategoryId.FOOD_AND_DRINK,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns primary category without subcategory', () => {
|
||||
const reader = new AppleConfigReader({ categories: [AppCategoryId.GAMES] });
|
||||
expect(reader.getCategories()).toMatchObject({
|
||||
primaryCategory: AppCategoryId.GAMES,
|
||||
secondaryCategory: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns primary category with two subcategories', () => {
|
||||
const reader = new AppleConfigReader({
|
||||
categories: [
|
||||
[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD, AppSubcategoryId.GAMES_ADVENTURE],
|
||||
],
|
||||
});
|
||||
expect(reader.getCategories()).toMatchObject({
|
||||
primaryCategory: AppCategoryId.GAMES,
|
||||
primarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
primarySubcategoryTwo: AppSubcategoryId.GAMES_ADVENTURE,
|
||||
secondaryCategory: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns primary and secondary categories with two subcategories', () => {
|
||||
const reader = new AppleConfigReader({
|
||||
categories: [
|
||||
AppCategoryId.ENTERTAINMENT,
|
||||
[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD, AppSubcategoryId.GAMES_ADVENTURE],
|
||||
],
|
||||
});
|
||||
expect(reader.getCategories()).toMatchObject({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: AppCategoryId.GAMES,
|
||||
secondarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
secondarySubcategoryTwo: AppSubcategoryId.GAMES_ADVENTURE,
|
||||
});
|
||||
expect(reader.getCategories()).not.toHaveProperty('primarySubcategoryOne');
|
||||
expect(reader.getCategories()).not.toHaveProperty('primarySubcategoryTwo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgeRating', () => {
|
||||
it('ignores advisory when not set', () => {
|
||||
const reader = new AppleConfigReader({ advisory: undefined });
|
||||
expect(reader.getAgeRating()).toBeNull();
|
||||
});
|
||||
|
||||
it('auto-fills least restrictive advisory', () => {
|
||||
const reader = new AppleConfigReader({ advisory: {} });
|
||||
expect(reader.getAgeRating()).toMatchObject(leastRestrictiveAdvisory);
|
||||
});
|
||||
|
||||
it('returns most restrictive advisory', () => {
|
||||
const reader = new AppleConfigReader({ advisory: mostRestrictiveAdvisory });
|
||||
expect(reader.getAgeRating()).toMatchObject(mostRestrictiveAdvisory);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import { AppCategoryId, AppSubcategoryId } from '@expo/apple-utils';
|
||||
|
||||
import { AppleConfigWriter } from '../writer';
|
||||
import {
|
||||
emptyAdvisory,
|
||||
kidsSixToEightAdvisory,
|
||||
leastRestrictiveAdvisory,
|
||||
mostRestrictiveAdvisory,
|
||||
} from './fixtures/ageRatingDeclaration';
|
||||
import { makeCategoryInfo } from './fixtures/appInfo';
|
||||
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';
|
||||
|
||||
describe('toSchema', () => {
|
||||
it('returns object with apple schema', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
expect(writer.toSchema()).toMatchObject({
|
||||
configVersion: 0,
|
||||
apple: expect.any(Object),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAgeRating', () => {
|
||||
it('auto-fills least restrictive advisory', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAgeRating(emptyAdvisory);
|
||||
expect(writer.schema.advisory).toMatchObject(leastRestrictiveAdvisory);
|
||||
});
|
||||
|
||||
it('modifies kids band rating', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAgeRating(kidsSixToEightAdvisory);
|
||||
expect(writer.schema.advisory).toMatchObject(kidsSixToEightAdvisory);
|
||||
});
|
||||
|
||||
it('modifies most restrictive advisory', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setAgeRating(mostRestrictiveAdvisory);
|
||||
expect(writer.schema.advisory).toMatchObject(mostRestrictiveAdvisory);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setInfoLocale', () => {
|
||||
it('creates and modifies the locale', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setInfoLocale(englishInfo);
|
||||
expect(writer.schema.info?.[englishInfo.locale]).toMatchObject({
|
||||
title: englishInfo.name,
|
||||
subtitle: englishInfo.subtitle,
|
||||
privacyPolicyUrl: englishInfo.privacyPolicyUrl,
|
||||
privacyPolicyText: englishInfo.privacyPolicyText,
|
||||
privacyChoicesUrl: englishInfo.privacyChoicesUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies existing locales', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setInfoLocale(englishInfo);
|
||||
writer.setInfoLocale(dutchInfo);
|
||||
writer.setInfoLocale({
|
||||
...englishInfo,
|
||||
name: 'This is now different',
|
||||
privacyPolicyText: null,
|
||||
});
|
||||
|
||||
expect(writer.schema.info?.[dutchInfo.locale]).toHaveProperty('title', dutchInfo.name);
|
||||
expect(writer.schema.info?.[englishInfo.locale]).toMatchObject({
|
||||
title: 'This is now different',
|
||||
privacyPolicyText: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCategories', () => {
|
||||
it('skips secondary category without primary category', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(makeCategoryInfo({}));
|
||||
expect(writer.schema.categories).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes existing values when undefined', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(makeCategoryInfo({ primaryCategory: AppCategoryId.BUSINESS }));
|
||||
writer.setCategories(makeCategoryInfo({}));
|
||||
expect(writer.schema.categories).toBeUndefined();
|
||||
});
|
||||
|
||||
it('modifies primary category only', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(makeCategoryInfo({ primaryCategory: AppCategoryId.ENTERTAINMENT }));
|
||||
expect(writer.schema.categories).toEqual([AppCategoryId.ENTERTAINMENT]);
|
||||
});
|
||||
|
||||
it('modifies primary and secondary categories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(
|
||||
makeCategoryInfo({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: AppCategoryId.FOOD_AND_DRINK,
|
||||
})
|
||||
);
|
||||
expect(writer.schema.categories).toEqual([
|
||||
AppCategoryId.ENTERTAINMENT,
|
||||
AppCategoryId.FOOD_AND_DRINK,
|
||||
]);
|
||||
});
|
||||
|
||||
it('modifies primary category without subcategories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(makeCategoryInfo({ primaryCategory: AppCategoryId.GAMES }));
|
||||
expect(writer.schema.categories).toEqual([AppCategoryId.GAMES]);
|
||||
});
|
||||
|
||||
it('modifies primary category with one subcategories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(
|
||||
makeCategoryInfo({
|
||||
primaryCategory: AppCategoryId.GAMES,
|
||||
primarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
})
|
||||
);
|
||||
expect(writer.schema.categories).toEqual([[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD]]);
|
||||
// We need to filter the lists to avoid writing `[GAMES, GAMES_CARD, null]`
|
||||
expect(writer.schema.categories![0]).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('modifies primary category with two subcategories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(
|
||||
makeCategoryInfo({
|
||||
primaryCategory: AppCategoryId.GAMES,
|
||||
primarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
primarySubcategoryTwo: AppSubcategoryId.GAMES_ADVENTURE,
|
||||
})
|
||||
);
|
||||
expect(writer.schema.categories).toEqual([
|
||||
[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD, AppSubcategoryId.GAMES_ADVENTURE],
|
||||
]);
|
||||
});
|
||||
|
||||
it('modifies primary and secondary categories with one subcategories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(
|
||||
makeCategoryInfo({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: AppCategoryId.GAMES,
|
||||
secondarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
})
|
||||
);
|
||||
expect(writer.schema.categories).toEqual([
|
||||
AppCategoryId.ENTERTAINMENT,
|
||||
[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD],
|
||||
]);
|
||||
// We need to filter the lists to avoid writing `[GAMES, GAMES_CARD, null]`
|
||||
expect(writer.schema.categories![1]).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('modifies primary and secondary categories with two subcategories', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(
|
||||
makeCategoryInfo({
|
||||
primaryCategory: AppCategoryId.ENTERTAINMENT,
|
||||
secondaryCategory: AppCategoryId.GAMES,
|
||||
secondarySubcategoryOne: AppSubcategoryId.GAMES_CARD,
|
||||
secondarySubcategoryTwo: AppSubcategoryId.GAMES_ADVENTURE,
|
||||
})
|
||||
);
|
||||
expect(writer.schema.categories).toEqual([
|
||||
AppCategoryId.ENTERTAINMENT,
|
||||
[AppCategoryId.GAMES, AppSubcategoryId.GAMES_CARD, AppSubcategoryId.GAMES_ADVENTURE],
|
||||
]);
|
||||
});
|
||||
|
||||
it('modifies secondary category without primary', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setCategories(makeCategoryInfo({ secondaryCategory: AppCategoryId.GAMES }));
|
||||
expect(writer.schema.categories).toEqual(['', AppCategoryId.GAMES]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVersion', () => {
|
||||
it('modifies the copyright', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersion(manualRelease);
|
||||
expect(writer.schema.copyright).toBe(manualRelease.copyright);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVersionRelease', () => {
|
||||
it('modifies scheduled release', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionRelease(scheduledRelease);
|
||||
expect(writer.schema.release).toMatchObject({
|
||||
autoReleaseDate: scheduledRelease.earliestReleaseDate,
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies automatic release', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionRelease(automaticRelease);
|
||||
expect(writer.schema.release).toMatchObject({
|
||||
automaticRelease: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not modify manual release', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionRelease(manualRelease);
|
||||
expect(writer.schema.release).toBeUndefined();
|
||||
});
|
||||
|
||||
it('overwrites all release fields', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionRelease(scheduledRelease);
|
||||
writer.setVersionRelease(automaticRelease);
|
||||
expect(writer.schema.release).not.toHaveProperty('autoReleaseDate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setVersionLocale', () => {
|
||||
it('creates and modifies the locale', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionLocale(englishVersion);
|
||||
expect(writer.schema.info?.[englishVersion.locale]).toMatchObject({
|
||||
description: englishVersion.description,
|
||||
keywords: englishVersion.keywords?.split(', '),
|
||||
releaseNotes: englishVersion.whatsNew,
|
||||
marketingUrl: englishVersion.marketingUrl,
|
||||
promoText: englishVersion.promotionalText,
|
||||
supportUrl: englishVersion.supportUrl,
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies existing locales', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setVersionLocale(englishVersion);
|
||||
writer.setVersionLocale(dutchVersion);
|
||||
writer.setVersionLocale({
|
||||
...englishVersion,
|
||||
description: 'This is now different',
|
||||
whatsNew: null,
|
||||
});
|
||||
|
||||
expect(writer.schema.info?.[dutchVersion.locale]).toHaveProperty(
|
||||
'description',
|
||||
dutchVersion.description
|
||||
);
|
||||
expect(writer.schema.info?.[englishVersion.locale]).toMatchObject({
|
||||
description: 'This is now different',
|
||||
releaseNotes: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setReviewDetails', () => {
|
||||
it('modifies name only review details', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setReviewDetails(nameOnlyReviewDetails);
|
||||
expect(writer.schema.review).toMatchObject({
|
||||
firstName: nameOnlyReviewDetails.contactFirstName,
|
||||
lastName: nameOnlyReviewDetails.contactLastName,
|
||||
email: nameOnlyReviewDetails.contactEmail,
|
||||
phone: nameOnlyReviewDetails.contactPhone,
|
||||
demoUsername: undefined,
|
||||
demoPassword: undefined,
|
||||
demoRequired: undefined,
|
||||
notes: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('modifies name and demo review details', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setReviewDetails(nameAndDemoReviewDetails);
|
||||
expect(writer.schema.review).toMatchObject({
|
||||
firstName: nameAndDemoReviewDetails.contactFirstName,
|
||||
lastName: nameAndDemoReviewDetails.contactLastName,
|
||||
email: nameAndDemoReviewDetails.contactEmail,
|
||||
phone: nameAndDemoReviewDetails.contactPhone,
|
||||
demoUsername: nameAndDemoReviewDetails.demoAccountName,
|
||||
demoPassword: nameAndDemoReviewDetails.demoAccountPassword,
|
||||
demoRequired: nameAndDemoReviewDetails.demoAccountRequired,
|
||||
notes: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('replaces existing review details', () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
writer.setReviewDetails(nameAndDemoReviewDetails);
|
||||
writer.setReviewDetails(nameOnlyReviewDetails);
|
||||
expect(writer.schema.review).toMatchObject({
|
||||
demoUsername: undefined,
|
||||
demoPassword: undefined,
|
||||
demoRequired: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
import {
|
||||
AgeRatingDeclaration,
|
||||
AppInfoLocalization,
|
||||
AppStoreReviewDetail,
|
||||
AppStoreVersion,
|
||||
AppStoreVersionLocalization,
|
||||
CategoryIds,
|
||||
Rating,
|
||||
ReleaseType,
|
||||
} from '@expo/apple-utils';
|
||||
|
||||
import uniq from '../../../utils/expodash/uniq';
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { removeDatePrecision } from '../../utils/date';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
type PartialExcept<T, K extends keyof T> = Pick<T, K> & Partial<Omit<T, K>>;
|
||||
|
||||
// TODO: find out if we can move this to default JSON schema normalization
|
||||
export const DEFAULT_WHATSNEW = 'Bug fixes and improved stability';
|
||||
|
||||
/**
|
||||
* Deserializes the metadata config schema into attributes for different models.
|
||||
* This uses version 0 of the config schema.
|
||||
*/
|
||||
export class AppleConfigReader {
|
||||
public constructor(public readonly schema: AppleMetadata) {}
|
||||
|
||||
public getAgeRating(): Partial<AttributesOf<AgeRatingDeclaration>> | null {
|
||||
const attributes = this.schema.advisory;
|
||||
if (!attributes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
alcoholTobaccoOrDrugUseOrReferences:
|
||||
attributes.alcoholTobaccoOrDrugUseOrReferences ?? Rating.NONE,
|
||||
contests: attributes.contests ?? Rating.NONE,
|
||||
gamblingSimulated: attributes.gamblingSimulated ?? Rating.NONE,
|
||||
horrorOrFearThemes: attributes.horrorOrFearThemes ?? Rating.NONE,
|
||||
matureOrSuggestiveThemes: attributes.matureOrSuggestiveThemes ?? Rating.NONE,
|
||||
medicalOrTreatmentInformation: attributes.medicalOrTreatmentInformation ?? Rating.NONE,
|
||||
profanityOrCrudeHumor: attributes.profanityOrCrudeHumor ?? Rating.NONE,
|
||||
sexualContentGraphicAndNudity: attributes.sexualContentGraphicAndNudity ?? Rating.NONE,
|
||||
sexualContentOrNudity: attributes.sexualContentOrNudity ?? Rating.NONE,
|
||||
violenceCartoonOrFantasy: attributes.violenceCartoonOrFantasy ?? Rating.NONE,
|
||||
violenceRealistic: attributes.violenceRealistic ?? Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic:
|
||||
attributes.violenceRealisticProlongedGraphicOrSadistic ?? Rating.NONE,
|
||||
gambling: attributes.gambling ?? false,
|
||||
unrestrictedWebAccess: attributes.unrestrictedWebAccess ?? false,
|
||||
kidsAgeBand: attributes.kidsAgeBand ?? null,
|
||||
seventeenPlus: attributes.seventeenPlus ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public getLocales(): string[] {
|
||||
// TODO: filter "default" locales, add option to add non-localized info to the config
|
||||
return uniq(Object.keys(this.schema.info || {}));
|
||||
}
|
||||
|
||||
public getInfoLocale(
|
||||
locale: string
|
||||
): PartialExcept<AttributesOf<AppInfoLocalization>, 'locale' | 'name'> | null {
|
||||
const info = this.schema.info?.[locale];
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
name: info.title,
|
||||
subtitle: info.subtitle,
|
||||
privacyChoicesUrl: info.privacyChoicesUrl,
|
||||
privacyPolicyText: info.privacyPolicyText,
|
||||
privacyPolicyUrl: info.privacyPolicyUrl,
|
||||
};
|
||||
}
|
||||
|
||||
public getCategories(): CategoryIds | null {
|
||||
const { categories } = this.schema;
|
||||
if (!categories || categories.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We validate the categories based on enums, but they will still be strings here.
|
||||
const categoryIds: Partial<Record<keyof CategoryIds, string>> = {};
|
||||
|
||||
if (Array.isArray(categories[0])) {
|
||||
categoryIds.primaryCategory = categories[0][0];
|
||||
categoryIds.primarySubcategoryOne = categories[0][1];
|
||||
categoryIds.primarySubcategoryTwo = categories[0][2];
|
||||
} else {
|
||||
categoryIds.primaryCategory = categories[0];
|
||||
}
|
||||
|
||||
if (Array.isArray(categories[1])) {
|
||||
categoryIds.secondaryCategory = categories[1][0];
|
||||
categoryIds.secondarySubcategoryOne = categories[1][1];
|
||||
categoryIds.secondarySubcategoryTwo = categories[1][2];
|
||||
} else {
|
||||
categoryIds.secondaryCategory = categories[1];
|
||||
}
|
||||
|
||||
// Because we handle categories as normal strings,
|
||||
// the type doesn't match with the actual CategoryIds types.
|
||||
return categoryIds as CategoryIds;
|
||||
}
|
||||
|
||||
/** Get the `AppStoreVersion` object. */
|
||||
public getVersion(): Partial<
|
||||
Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
|
||||
> | null {
|
||||
return this.schema.copyright ? { copyright: this.schema.copyright } : null;
|
||||
}
|
||||
|
||||
public getVersionRelease(): Partial<
|
||||
Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
|
||||
> | null {
|
||||
const { release } = this.schema;
|
||||
|
||||
if (release?.autoReleaseDate) {
|
||||
return {
|
||||
releaseType: ReleaseType.SCHEDULED,
|
||||
// Convert time format to 2020-06-17T12:00:00-07:00
|
||||
earliestReleaseDate: removeDatePrecision(release.autoReleaseDate)?.toISOString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
if (release?.automaticRelease === true) {
|
||||
return {
|
||||
releaseType: ReleaseType.AFTER_APPROVAL,
|
||||
earliestReleaseDate: null,
|
||||
};
|
||||
}
|
||||
|
||||
if (release?.automaticRelease === false) {
|
||||
return {
|
||||
releaseType: ReleaseType.MANUAL,
|
||||
earliestReleaseDate: null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public getVersionLocale(
|
||||
locale: string,
|
||||
context: { versionIsFirst: boolean }
|
||||
): Partial<AttributesOf<AppStoreVersionLocalization>> | null {
|
||||
const info = this.schema.info?.[locale];
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
locale,
|
||||
description: info.description,
|
||||
keywords: info.keywords?.join(', '),
|
||||
// TODO: maybe move this to task logic, it's more an exception than data handling
|
||||
whatsNew: context.versionIsFirst ? undefined : info.releaseNotes || DEFAULT_WHATSNEW,
|
||||
marketingUrl: info.marketingUrl,
|
||||
promotionalText: info.promoText,
|
||||
supportUrl: info.supportUrl,
|
||||
};
|
||||
}
|
||||
|
||||
public getReviewDetails(): Partial<AttributesOf<AppStoreReviewDetail>> | null {
|
||||
const review = this.schema.review;
|
||||
if (!review) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
contactFirstName: review.firstName,
|
||||
contactLastName: review.lastName,
|
||||
contactEmail: review.email,
|
||||
contactPhone: review.phone,
|
||||
demoAccountName: review.demoUsername,
|
||||
demoAccountPassword: review.demoPassword,
|
||||
demoAccountRequired: review.demoRequired,
|
||||
notes: review.notes,
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
AgeRatingDeclaration,
|
||||
AppInfo,
|
||||
AppInfoLocalization,
|
||||
AppStoreReviewDetail,
|
||||
AppStoreVersion,
|
||||
AppStoreVersionLocalization,
|
||||
Rating,
|
||||
ReleaseType,
|
||||
} from '@expo/apple-utils';
|
||||
|
||||
import { AttributesOf } from '../../utils/asc';
|
||||
import { AppleMetadata } from '../types';
|
||||
|
||||
/**
|
||||
* Serializes the Apple ASC entities into the metadata configuration schema.
|
||||
* This uses version 0 of the config schema.
|
||||
*/
|
||||
export class AppleConfigWriter {
|
||||
constructor(public readonly schema: Partial<AppleMetadata> = {}) {}
|
||||
|
||||
/** Get the schema result to write it to the config file */
|
||||
public toSchema(): { configVersion: number; apple: Partial<AppleMetadata> } {
|
||||
return {
|
||||
configVersion: 0,
|
||||
apple: this.schema,
|
||||
};
|
||||
}
|
||||
|
||||
public setAgeRating(attributes: AttributesOf<AgeRatingDeclaration>): void {
|
||||
this.schema.advisory = {
|
||||
alcoholTobaccoOrDrugUseOrReferences:
|
||||
attributes.alcoholTobaccoOrDrugUseOrReferences ?? Rating.NONE,
|
||||
contests: attributes.contests ?? Rating.NONE,
|
||||
gamblingSimulated: attributes.gamblingSimulated ?? Rating.NONE,
|
||||
horrorOrFearThemes: attributes.horrorOrFearThemes ?? Rating.NONE,
|
||||
matureOrSuggestiveThemes: attributes.matureOrSuggestiveThemes ?? Rating.NONE,
|
||||
medicalOrTreatmentInformation: attributes.medicalOrTreatmentInformation ?? Rating.NONE,
|
||||
profanityOrCrudeHumor: attributes.profanityOrCrudeHumor ?? Rating.NONE,
|
||||
sexualContentGraphicAndNudity: attributes.sexualContentGraphicAndNudity ?? Rating.NONE,
|
||||
sexualContentOrNudity: attributes.sexualContentOrNudity ?? Rating.NONE,
|
||||
violenceCartoonOrFantasy: attributes.violenceCartoonOrFantasy ?? Rating.NONE,
|
||||
violenceRealistic: attributes.violenceRealistic ?? Rating.NONE,
|
||||
violenceRealisticProlongedGraphicOrSadistic:
|
||||
attributes.violenceRealisticProlongedGraphicOrSadistic ?? Rating.NONE,
|
||||
gambling: attributes.gambling ?? false,
|
||||
unrestrictedWebAccess: attributes.unrestrictedWebAccess ?? false,
|
||||
kidsAgeBand: attributes.kidsAgeBand ?? null,
|
||||
seventeenPlus: attributes.seventeenPlus ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public setInfoLocale(attributes: AttributesOf<AppInfoLocalization>): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
const existing = this.schema.info[attributes.locale] ?? {};
|
||||
|
||||
this.schema.info[attributes.locale] = {
|
||||
...existing,
|
||||
title: attributes.name ?? '',
|
||||
subtitle: optional(attributes.subtitle),
|
||||
privacyPolicyUrl: optional(attributes.privacyPolicyUrl),
|
||||
privacyPolicyText: optional(attributes.privacyPolicyText),
|
||||
privacyChoicesUrl: optional(attributes.privacyChoicesUrl),
|
||||
};
|
||||
}
|
||||
|
||||
public setCategories(
|
||||
attributes: Pick<
|
||||
AttributesOf<AppInfo>,
|
||||
| 'primaryCategory'
|
||||
| 'primarySubcategoryOne'
|
||||
| 'primarySubcategoryTwo'
|
||||
| 'secondaryCategory'
|
||||
| 'secondarySubcategoryOne'
|
||||
| 'secondarySubcategoryTwo'
|
||||
>
|
||||
): void {
|
||||
this.schema.categories = undefined;
|
||||
if (!attributes.primaryCategory && !attributes.secondaryCategory) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.schema.categories = [];
|
||||
|
||||
if (attributes.primaryCategory && attributes.primarySubcategoryOne) {
|
||||
this.schema.categories[0] = [
|
||||
attributes.primaryCategory.id,
|
||||
attributes.primarySubcategoryOne?.id,
|
||||
attributes.primarySubcategoryTwo?.id,
|
||||
].filter(Boolean) as string[];
|
||||
} else {
|
||||
// If only the secondaryCategory was provided,
|
||||
// autofill with an empty string and cause a store config error.
|
||||
this.schema.categories[0] = attributes.primaryCategory?.id ?? '';
|
||||
}
|
||||
|
||||
if (attributes.secondaryCategory && attributes.secondarySubcategoryOne) {
|
||||
this.schema.categories[1] = [
|
||||
attributes.secondaryCategory.id,
|
||||
attributes.secondarySubcategoryOne?.id,
|
||||
attributes.secondarySubcategoryTwo?.id,
|
||||
].filter(Boolean) as string[];
|
||||
} else if (attributes.secondaryCategory) {
|
||||
this.schema.categories[1] = attributes.secondaryCategory.id;
|
||||
}
|
||||
}
|
||||
|
||||
public setVersion(
|
||||
attributes: Omit<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
|
||||
): void {
|
||||
this.schema.copyright = optional(attributes.copyright);
|
||||
}
|
||||
|
||||
public setVersionRelease(
|
||||
attributes: Pick<AttributesOf<AppStoreVersion>, 'releaseType' | 'earliestReleaseDate'>
|
||||
): void {
|
||||
if (attributes.releaseType === ReleaseType.SCHEDULED) {
|
||||
this.schema.release = {
|
||||
autoReleaseDate: optional(attributes.earliestReleaseDate),
|
||||
};
|
||||
}
|
||||
|
||||
if (attributes.releaseType === ReleaseType.AFTER_APPROVAL) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public setVersionLocale(attributes: AttributesOf<AppStoreVersionLocalization>): void {
|
||||
this.schema.info = this.schema.info ?? {};
|
||||
const existing = this.schema.info[attributes.locale] ?? {};
|
||||
|
||||
this.schema.info[attributes.locale] = {
|
||||
...existing,
|
||||
description: optional(attributes.description),
|
||||
keywords: optional(attributes.keywords)
|
||||
?.split(',')
|
||||
.map(keyword => keyword.trim()),
|
||||
releaseNotes: optional(attributes.whatsNew),
|
||||
marketingUrl: optional(attributes.marketingUrl),
|
||||
promoText: optional(attributes.promotionalText),
|
||||
supportUrl: optional(attributes.supportUrl),
|
||||
};
|
||||
}
|
||||
|
||||
public setReviewDetails(attributes: AttributesOf<AppStoreReviewDetail>): void {
|
||||
this.schema.review = {
|
||||
firstName: attributes.contactFirstName ?? '',
|
||||
lastName: attributes.contactLastName ?? '',
|
||||
email: attributes.contactEmail ?? '',
|
||||
phone: attributes.contactPhone ?? '',
|
||||
demoUsername: optional(attributes.demoAccountName),
|
||||
demoPassword: optional(attributes.demoAccountPassword),
|
||||
demoRequired: optional(attributes.demoAccountRequired),
|
||||
notes: optional(attributes.notes),
|
||||
// TODO: add attachment
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to convert `T | null` to `T | undefined`, required for the entity properties */
|
||||
function optional<T>(value: T | null): T | undefined {
|
||||
return value ?? undefined;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { App } from '@expo/apple-utils';
|
||||
|
||||
import type { AgeRatingData } from './tasks/age-rating';
|
||||
import type { AppInfoData } from './tasks/app-info';
|
||||
import type { AppReviewData } from './tasks/app-review-detail';
|
||||
import type { AppVersionData } from './tasks/app-version';
|
||||
|
||||
/**
|
||||
* The fully prepared apple data, used within the `downloadAsync` or `uploadAsync` tasks.
|
||||
* It contains references to each individual models, to either upload or download App Store data.
|
||||
*/
|
||||
export type AppleData = { app: App } & AppInfoData & AppVersionData & AgeRatingData & AppReviewData;
|
||||
|
||||
/**
|
||||
* The unprepared partial apple data, used within the `prepareAsync` tasks.
|
||||
* It contains a reference to the app, each task should populate the necessary data.
|
||||
* If an entity fails to prepare the data, individual tasks should raise errors about the missing data.
|
||||
*/
|
||||
export type PartialAppleData = { app: App } & Partial<Omit<AppleData, 'app'>>;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AppleConfigReader } from './config/reader';
|
||||
import { AppleConfigWriter } from './config/writer';
|
||||
import { AppleData, PartialAppleData } from './data';
|
||||
|
||||
export abstract class AppleTask {
|
||||
/** Get a description from the task to use as section headings in the log */
|
||||
public abstract name(): string;
|
||||
|
||||
/** Prepare the data from the App Store to start syncing with the store configuration */
|
||||
public abstract prepareAsync(options: TaskPrepareOptions): Promise<void>;
|
||||
|
||||
/** Download all information from the App Store to generate the store configuration */
|
||||
public abstract downloadAsync(options: TaskDownloadOptions): Promise<void>;
|
||||
|
||||
/** Upload all information from the store configuration to the App Store */
|
||||
public abstract uploadAsync(options: TaskUploadOptions): Promise<void>;
|
||||
}
|
||||
|
||||
export type TaskPrepareOptions = {
|
||||
context: PartialAppleData;
|
||||
};
|
||||
|
||||
export type TaskDownloadOptions = {
|
||||
config: AppleConfigWriter;
|
||||
context: AppleData;
|
||||
};
|
||||
|
||||
export type TaskUploadOptions = {
|
||||
config: AppleConfigReader;
|
||||
context: AppleData;
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import { getRequestClient } from '@expo/apple-utils';
|
||||
|
||||
export { default } from 'nock';
|
||||
|
||||
// Axios from apple-utils needs to be patched when we use Nock.
|
||||
// Instead of the default adapter, we need to use `axios/lib/adapters/http`
|
||||
// see: https://github.com/nock/nock#axios
|
||||
getRequestClient().defaults.adapter = require('axios/lib/adapters/http');
|
||||
@@ -0,0 +1,111 @@
|
||||
import { AgeRatingDeclaration, AppStoreVersion, Rating } from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { AppleData } from '../../data';
|
||||
import { AgeRatingTask } from '../age-rating';
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
describe(AgeRatingTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AgeRatingTask().prepareAsync({ context: {} as any });
|
||||
|
||||
await expect(promise).rejects.toThrow('not prepared');
|
||||
});
|
||||
|
||||
it('loads age rating from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
|
||||
.reply(200, require('./fixtures/appStoreVersions/get-ageRatingDeclaration-200.json'));
|
||||
|
||||
const context: any = {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AgeRatingTask().prepareAsync({ context });
|
||||
|
||||
expect(context.ageRating).toBeInstanceOf(AgeRatingDeclaration);
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('skips age rating when not prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AgeRatingTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { ageRating: undefined } as any,
|
||||
});
|
||||
|
||||
expect(writer.setAgeRating).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sets age rating when prepared', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const ageRating = new AgeRatingDeclaration(requestContext, 'stub-id', {} as any);
|
||||
|
||||
await new AgeRatingTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { ageRating } as any,
|
||||
});
|
||||
|
||||
expect(writer.setAgeRating).toBeCalledWith(ageRating.attributes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('aborts when age rating is not loaded', async () => {
|
||||
const promise = new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { ageRating: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('rating not initialized');
|
||||
});
|
||||
|
||||
it('skips updating age rating when not configured', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`)
|
||||
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json'));
|
||||
|
||||
await new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({ advisory: undefined }),
|
||||
context: {
|
||||
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any),
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeFalsy();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('updates age rating from config when configured', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AgeRatingDeclaration.type}/stub-id`)
|
||||
.reply(200, require('./fixtures/ageRatingDeclarations/patch-200.json'));
|
||||
|
||||
const context = {
|
||||
ageRating: new AgeRatingDeclaration(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AgeRatingTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
advisory: {
|
||||
// See fixture value for horrorOrFearThemes
|
||||
horrorOrFearThemes: Rating.INFREQUENT_OR_MILD,
|
||||
},
|
||||
}),
|
||||
context: context as AppleData,
|
||||
});
|
||||
|
||||
expect(context.ageRating.id).not.toMatch('stub-id');
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
import { App, AppCategoryId, AppInfo, AppInfoLocalization } from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { AppleData, PartialAppleData } from '../../data';
|
||||
import { AppInfoTask } from '../app-info';
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
describe(AppInfoTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('loads editable app info and locales from app instance', async () => {
|
||||
const scopeInfo = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(uri => uri.startsWith(`/v1/${App.type}/stub-id/${AppInfo.type}`)) // allow any query params
|
||||
.reply(200, require('./fixtures/apps/get-appInfos-200.json'));
|
||||
|
||||
const scopeLocales = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(/\/v1\/appInfos\/.*\/appInfoLocalizations/) // allow any id from fixture
|
||||
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json'));
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppInfoTask().prepareAsync({ context });
|
||||
|
||||
expect(context.info).toBeInstanceOf(AppInfo);
|
||||
expect(context.infoLocales).toBeInstanceOf(Array);
|
||||
expect(scopeInfo.isDone()).toBeTruthy();
|
||||
expect(scopeLocales.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('aborts when app info is not loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const promise = new AppInfoTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { info: undefined, infoLocales: [] } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('info not initialized');
|
||||
});
|
||||
|
||||
it('sets categories when info is loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const info = new AppInfo(requestContext, 'stub-id', {} as any);
|
||||
|
||||
await new AppInfoTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { info, infoLocales: [] } as any,
|
||||
});
|
||||
|
||||
expect(writer.setCategories).toBeCalledWith(info.attributes);
|
||||
});
|
||||
|
||||
it('skips when no locales are loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AppInfoTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
infoLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setInfoLocale).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sets locales when loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const infoLocales = [
|
||||
new AppInfoLocalization(requestContext, 'stub-id-1', {} as any),
|
||||
new AppInfoLocalization(requestContext, 'stub-id-2', {} as any),
|
||||
];
|
||||
|
||||
await new AppInfoTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
infoLocales,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setInfoLocale).toBeCalledWith(infoLocales[0].attributes);
|
||||
expect(writer.setInfoLocale).toBeCalledWith(infoLocales[1].attributes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('aborts when app info is not loaded', async () => {
|
||||
const promise = new AppInfoTask().uploadAsync({
|
||||
config: new AppleConfigReader({}),
|
||||
context: { info: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('info not initialized');
|
||||
});
|
||||
|
||||
it('skips updating categories when not configured', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppInfo.type}/stub-id`)
|
||||
.reply(200, require('./fixtures/appInfos/patch-200.json'));
|
||||
|
||||
await new AppInfoTask().uploadAsync({
|
||||
config: new AppleConfigReader({ categories: undefined }),
|
||||
context: {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeFalsy();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('skips updating localized info when not configured', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppInfoLocalization.type}/stub-id`)
|
||||
.reply(200, require('./fixtures/appInfos/patch-200.json'));
|
||||
|
||||
await new AppInfoTask().uploadAsync({
|
||||
config: new AppleConfigReader({ info: undefined }),
|
||||
context: {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeFalsy();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('updates categories and localized info', async () => {
|
||||
const updateCategoryScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppInfo.type}/stub-id`)
|
||||
.reply(200, require('./fixtures/appInfos/patch-200.json'));
|
||||
|
||||
const updateENInfoScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_1`) // see fixture ID
|
||||
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json'));
|
||||
|
||||
const updateNLInfoScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppInfoLocalization.type}/APP_INFO_LOCALE_2`) // see fixture ID
|
||||
.reply(200, require('./fixtures/appInfoLocalizations/patch-200.json'));
|
||||
|
||||
const fetchLocalizedInfoScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppInfo.type}/APP_INFO_1/${AppInfoLocalization.type}`) // see fixture ID
|
||||
.twice()
|
||||
.reply(200, require('./fixtures/appInfos/get-appInfoLocalizations-200.json'));
|
||||
|
||||
await new AppInfoTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
categories: [AppCategoryId.ENTERTAINMENT],
|
||||
info: {
|
||||
'en-US': { title: 'Hello' },
|
||||
'nl-NL': { title: 'Hallo' },
|
||||
},
|
||||
}),
|
||||
context: {
|
||||
info: new AppInfo(requestContext, 'stub-id', {} as any),
|
||||
} as AppleData,
|
||||
});
|
||||
|
||||
expect(updateCategoryScope.isDone()).toBeTruthy();
|
||||
expect(fetchLocalizedInfoScope.isDone()).toBeTruthy();
|
||||
expect(updateENInfoScope.isDone()).toBeTruthy();
|
||||
expect(updateNLInfoScope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { AppStoreReviewAttachment, AppStoreReviewDetail, AppStoreVersion } from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { AppReviewDetailTask } from '../app-review-detail';
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
describe(AppReviewDetailTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppReviewDetailTask().prepareAsync({ context: {} as any });
|
||||
|
||||
await expect(promise).rejects.toThrow('version not init');
|
||||
});
|
||||
|
||||
it('loads review details from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/appStoreReviewDetail`)
|
||||
.query((params: any) => params['include'] === AppStoreReviewAttachment.type)
|
||||
.reply(200, require('./fixtures/appStoreVersions/get-appStoreReviewDetail-200.json'));
|
||||
|
||||
const context: any = {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppReviewDetailTask().prepareAsync({ context });
|
||||
|
||||
expect(context.reviewDetail).toBeInstanceOf(AppStoreReviewDetail);
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('sets review details when loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const reviewDetail = new AppStoreReviewDetail(requestContext, 'stub-id', {} as any);
|
||||
|
||||
await new AppReviewDetailTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { reviewDetail } as any,
|
||||
});
|
||||
|
||||
expect(writer.setReviewDetails).toBeCalledWith(reviewDetail.attributes);
|
||||
});
|
||||
|
||||
it('skips when no review details are loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AppReviewDetailTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {} as any,
|
||||
});
|
||||
|
||||
expect(writer.setReviewDetails).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('skips when review details are not configured', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.post(`/v1/${AppStoreReviewDetail.type}`)
|
||||
.reply(201, require('./fixtures/appStoreReviewDetails/post-201.json'));
|
||||
|
||||
await new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: undefined,
|
||||
}),
|
||||
context: {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeFalsy();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: '[email protected]',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {} as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('version not init');
|
||||
});
|
||||
|
||||
it('updates review details when loaded', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppStoreReviewDetail.type}/APP_STORE_REVIEW_DETAILS_1`)
|
||||
.reply(200, require('./fixtures/appStoreReviewDetails/patch-200.json'));
|
||||
|
||||
await new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: '[email protected]',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
reviewDetail: new AppStoreReviewDetail(
|
||||
requestContext,
|
||||
'APP_STORE_REVIEW_DETAILS_1',
|
||||
{} as any
|
||||
),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('creates and updates review details when not loaded', async () => {
|
||||
const createReviewDetailScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.post(`/v1/${AppStoreReviewDetail.type}`)
|
||||
.reply(201, require('./fixtures/appStoreReviewDetails/post-201.json'));
|
||||
|
||||
const updateReviewDetailScope = nock('https://api.appstoreconnect.apple.com')
|
||||
.patch(`/v1/${AppStoreReviewDetail.type}/APP_STORE_REVIEW_DETAILS_1`)
|
||||
.reply(200, require('./fixtures/appStoreReviewDetails/patch-200.json'));
|
||||
|
||||
await new AppReviewDetailTask().uploadAsync({
|
||||
config: new AppleConfigReader({
|
||||
review: {
|
||||
firstName: 'Evan',
|
||||
lastName: 'Bacon',
|
||||
email: '[email protected]',
|
||||
phone: '+1 555 555 5555',
|
||||
},
|
||||
}),
|
||||
context: {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(createReviewDetailScope.isDone()).toBeTruthy();
|
||||
expect(updateReviewDetailScope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
import {
|
||||
App,
|
||||
AppStoreState,
|
||||
AppStoreVersion,
|
||||
AppStoreVersionLocalization,
|
||||
} from '@expo/apple-utils';
|
||||
import nock from 'nock';
|
||||
|
||||
import { AppleConfigReader } from '../../config/reader';
|
||||
import { AppleConfigWriter } from '../../config/writer';
|
||||
import { AppleData, PartialAppleData } from '../../data';
|
||||
import { AppVersionTask } from '../app-version';
|
||||
import { requestContext } from './fixtures/requestContext';
|
||||
|
||||
jest.mock('../../../../ora');
|
||||
jest.mock('../../config/writer');
|
||||
|
||||
describe(AppVersionTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('loads live version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Respond to app.getLiveAppStoreVersionAsync
|
||||
.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-200.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-200.json'))
|
||||
// Respond to version.getLocalizationsAsync
|
||||
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
|
||||
.reply(
|
||||
200,
|
||||
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
|
||||
);
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
|
||||
expect(context.version).toBeInstanceOf(AppStoreVersion);
|
||||
expect(context.versionIsLive).toBeTruthy();
|
||||
expect(context.versionIsFirst).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('falls back to editable version if live version is not found', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Respond to app.getLiveAppStoreVersionAsync, failing with 404
|
||||
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
|
||||
.query((params: any) => params['filter[appStoreState]'] === AppStoreState.READY_FOR_SALE)
|
||||
.reply(404, {})
|
||||
// Respond to app.getEditAppStoreVersionAsync
|
||||
.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 app.getAppStoreVersionsAsync
|
||||
.get(`/v1/${App.type}/stub-id/${AppStoreVersion.type}`)
|
||||
.query(true)
|
||||
.reply(200, require('./fixtures/apps/get-appStoreVersions-count-200.json'))
|
||||
// Respond to version.getLocalizationsAsync
|
||||
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
|
||||
.reply(
|
||||
200,
|
||||
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
|
||||
);
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppVersionTask({ editLive: true }).prepareAsync({ context });
|
||||
|
||||
expect(context.version).toBeInstanceOf(AppStoreVersion);
|
||||
expect(context.versionIsLive).toBeFalsy();
|
||||
expect(context.versionIsFirst).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('loads last editable version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Respond to app.getEditAppStoreVersionAsync
|
||||
.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-200.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-200.json'))
|
||||
// Respond to version.getLocalizationsAsync
|
||||
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
|
||||
.reply(
|
||||
200,
|
||||
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
|
||||
);
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
|
||||
expect(context.version).toBeInstanceOf(AppStoreVersion);
|
||||
expect(context.versionIsLive).toBeFalsy();
|
||||
expect(context.versionIsFirst).toBeTruthy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('sets first version property for multiple versions', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Respond to app.getEditAppStoreVersionAsync
|
||||
.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-200.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
|
||||
.get(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1/${AppStoreVersionLocalization.type}`)
|
||||
.reply(
|
||||
200,
|
||||
require('./fixtures/appStoreVersions/get-appStoreVersionLocalizations-200.json')
|
||||
);
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
};
|
||||
|
||||
await new AppVersionTask().prepareAsync({ context });
|
||||
|
||||
expect(context.versionIsFirst).toBeFalsy();
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppVersionTask().downloadAsync({
|
||||
config: new AppleConfigWriter(),
|
||||
context: { version: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('not initialized');
|
||||
});
|
||||
|
||||
it('sets version when loaded', async () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
const version = new AppStoreVersion(requestContext, 'stub-id', {} as any);
|
||||
|
||||
await new AppVersionTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { version, versionLocales: [] } as any,
|
||||
});
|
||||
|
||||
expect(writer.setVersion).toBeCalledWith(version.attributes);
|
||||
});
|
||||
|
||||
it('sets version release when loaded', async () => {
|
||||
const writer = new AppleConfigWriter();
|
||||
const version = new AppStoreVersion(requestContext, 'stub-id', {} as any);
|
||||
|
||||
await new AppVersionTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { version, versionLocales: [] } as any,
|
||||
});
|
||||
|
||||
expect(writer.setVersionRelease).toBeCalledWith(version.attributes);
|
||||
});
|
||||
|
||||
it('skips when no locales are loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AppVersionTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
versionLocales: [],
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setInfoLocale).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sets locales when loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const versionLocales = [
|
||||
new AppStoreVersionLocalization(requestContext, 'stub-id-1', {} as any),
|
||||
new AppStoreVersionLocalization(requestContext, 'stub-id-2', {} as any),
|
||||
];
|
||||
|
||||
await new AppVersionTask().downloadAsync({
|
||||
config: writer,
|
||||
context: {
|
||||
version: new AppStoreVersion(requestContext, 'stub-id', {} as any),
|
||||
versionLocales,
|
||||
} as any,
|
||||
});
|
||||
|
||||
expect(writer.setVersionLocale).toBeCalledWith(versionLocales[0].attributes);
|
||||
expect(writer.setVersionLocale).toBeCalledWith(versionLocales[1].attributes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uploadAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppVersionTask().uploadAsync({
|
||||
context: {} as any,
|
||||
config: new AppleConfigReader({}),
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('not initialized');
|
||||
});
|
||||
|
||||
it('updates version and release from config', async () => {
|
||||
const config = new AppleConfigReader({
|
||||
copyright: '2022 ACME',
|
||||
release: { automaticRelease: true },
|
||||
});
|
||||
|
||||
const attributes = { ...config.getVersion(), ...config.getVersionRelease() };
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
// Respond to version.updateAsync
|
||||
.patch(`/v1/${AppStoreVersion.type}/APP_STORE_VERSION_1`)
|
||||
.reply(200, {
|
||||
data: {
|
||||
id: 'APP_STORE_VERSION_1',
|
||||
type: AppStoreVersion.type,
|
||||
attributes,
|
||||
},
|
||||
});
|
||||
|
||||
const context: PartialAppleData = {
|
||||
app: new App(requestContext, 'stub-id', {} as any),
|
||||
version: new AppStoreVersion(requestContext, 'APP_STORE_VERSION_1', {} as any),
|
||||
};
|
||||
|
||||
await new AppVersionTask().uploadAsync({ context: context as AppleData, config });
|
||||
|
||||
expect(context.version?.attributes).toMatchObject(attributes);
|
||||
expect(scope.isDone()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "ageRatingDeclarations",
|
||||
"id": "AGE_RATING_1",
|
||||
"attributes": {
|
||||
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
|
||||
"contests": "NONE",
|
||||
"gamblingAndContests": false,
|
||||
"gambling": false,
|
||||
"gamblingSimulated": "NONE",
|
||||
"kidsAgeBand": null,
|
||||
"medicalOrTreatmentInformation": "NONE",
|
||||
"profanityOrCrudeHumor": "NONE",
|
||||
"sexualContentGraphicAndNudity": "NONE",
|
||||
"sexualContentOrNudity": "NONE",
|
||||
"seventeenPlus": false,
|
||||
"horrorOrFearThemes": "INFREQUENT_OR_MILD",
|
||||
"matureOrSuggestiveThemes": "NONE",
|
||||
"unrestrictedWebAccess": false,
|
||||
"violenceCartoonOrFantasy": "NONE",
|
||||
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
|
||||
"violenceRealistic": "NONE"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/AGE_RATING_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/AGE_RATING_1"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "appInfoLocalizations",
|
||||
"id": "APP_INFO_LOCALE_1",
|
||||
"attributes": {
|
||||
"locale": "en-US",
|
||||
"name": "Rexpo Square App",
|
||||
"subtitle": "144 fps red squares",
|
||||
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
|
||||
"privacyChoicesUrl": null,
|
||||
"privacyPolicyText": null
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "appInfoLocalizations",
|
||||
"id": "APP_INFO_LOCALE_1",
|
||||
"attributes": {
|
||||
"locale": "en-US",
|
||||
"name": "Rexpo Square App",
|
||||
"subtitle": "144 fps red squares",
|
||||
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
|
||||
"privacyChoicesUrl": null,
|
||||
"privacyPolicyText": null
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "appInfoLocalizations",
|
||||
"id": "APP_INFO_LOCALE_2",
|
||||
"attributes": {
|
||||
"locale": "nl-NL",
|
||||
"name": "Rexpo Vierkant App",
|
||||
"subtitle": "144 fps red vierkant",
|
||||
"privacyPolicyUrl": "https://github.com/bycedric/red-square-app",
|
||||
"privacyChoicesUrl": null,
|
||||
"privacyPolicyText": null
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfoLocalizations/APP_INFO_LOCALE_2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/a9c2ad6a-182f-4021-9456-6b609ebc5eab/appInfoLocalizations"
|
||||
},
|
||||
"meta": {
|
||||
"paging": {
|
||||
"total": 2,
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "appInfos",
|
||||
"id": "APP_INFO_1",
|
||||
"attributes": {
|
||||
"appStoreState": "PREPARE_FOR_SUBMISSION",
|
||||
"appStoreAgeRating": "NINE_PLUS",
|
||||
"brazilAgeRating": "TEN",
|
||||
"kidsAgeBand": null
|
||||
},
|
||||
"relationships": {
|
||||
"ageRatingDeclaration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/ageRatingDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/ageRatingDeclaration"
|
||||
}
|
||||
},
|
||||
"appInfoLocalizations": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/appInfoLocalizations",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/appInfoLocalizations"
|
||||
}
|
||||
},
|
||||
"primaryCategory": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primaryCategory",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primaryCategory"
|
||||
}
|
||||
},
|
||||
"primarySubcategoryOne": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryOne",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryOne"
|
||||
}
|
||||
},
|
||||
"primarySubcategoryTwo": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryTwo",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryTwo"
|
||||
}
|
||||
},
|
||||
"secondaryCategory": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondaryCategory",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondaryCategory"
|
||||
}
|
||||
},
|
||||
"secondarySubcategoryOne": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryOne",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryOne"
|
||||
}
|
||||
},
|
||||
"secondarySubcategoryTwo": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryTwo",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryTwo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1"
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "appStoreReviewDetails",
|
||||
"id": "APP_STORE_REVIEW_DETAILS_1",
|
||||
"attributes": {
|
||||
"contactFirstName": "Evan",
|
||||
"contactLastName": "Bacon",
|
||||
"contactPhone": "+1-555-555-5555",
|
||||
"contactEmail": "[email protected]",
|
||||
"demoAccountName": null,
|
||||
"demoAccountPassword": null,
|
||||
"demoAccountRequired": false,
|
||||
"notes": ""
|
||||
},
|
||||
"relationships": {
|
||||
"appStoreReviewAttachments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appStoreReviewAttachments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appStoreReviewAttachments"
|
||||
}
|
||||
},
|
||||
"appSandboxEntitlementUsages": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appSandboxEntitlementUsages",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appSandboxEntitlementUsages"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1"
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "appStoreReviewDetails",
|
||||
"id": "APP_STORE_REVIEW_DETAILS_1",
|
||||
"attributes": {
|
||||
"contactFirstName": "Evan",
|
||||
"contactLastName": "Bacon",
|
||||
"contactPhone": "+1-555-555-5555",
|
||||
"contactEmail": "[email protected]",
|
||||
"demoAccountName": null,
|
||||
"demoAccountPassword": null,
|
||||
"demoAccountRequired": false,
|
||||
"notes": ""
|
||||
},
|
||||
"relationships": {
|
||||
"appStoreReviewAttachments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appStoreReviewAttachments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appStoreReviewAttachments"
|
||||
}
|
||||
},
|
||||
"appSandboxEntitlementUsages": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appSandboxEntitlementUsages",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appSandboxEntitlementUsages"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails"
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "ageRatingDeclarations",
|
||||
"id": "a9c2ad6a-182f-4021-9456-6b609ebc5eab",
|
||||
"attributes": {
|
||||
"alcoholTobaccoOrDrugUseOrReferences": "NONE",
|
||||
"contests": "NONE",
|
||||
"gamblingAndContests": false,
|
||||
"gambling": false,
|
||||
"gamblingSimulated": "NONE",
|
||||
"kidsAgeBand": null,
|
||||
"medicalOrTreatmentInformation": "NONE",
|
||||
"profanityOrCrudeHumor": "NONE",
|
||||
"sexualContentGraphicAndNudity": "NONE",
|
||||
"sexualContentOrNudity": "NONE",
|
||||
"seventeenPlus": false,
|
||||
"horrorOrFearThemes": "NONE",
|
||||
"matureOrSuggestiveThemes": "NONE",
|
||||
"unrestrictedWebAccess": false,
|
||||
"violenceCartoonOrFantasy": "NONE",
|
||||
"violenceRealisticProlongedGraphicOrSadistic": "NONE",
|
||||
"violenceRealistic": "NONE"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/ageRatingDeclarations/a9c2ad6a-182f-4021-9456-6b609ebc5eab"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/949768f6-58e5-46b4-b046-23861f61831a/ageRatingDeclaration"
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"id": "9901eeb2-81e7-4562-9beb-c5f72010ab71",
|
||||
"status": "404",
|
||||
"code": "NOT_FOUND",
|
||||
"title": "The specified resource does not exist",
|
||||
"detail": "There is no resource of type 'appStoreVersions' with id 'non-existing'"
|
||||
}
|
||||
]
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreReviewDetail?include=appStoreReviewAttachments"
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"data": {
|
||||
"type": "appStoreReviewDetails",
|
||||
"id": "APP_STORE_REVIEW_DETAILS_1",
|
||||
"attributes": {
|
||||
"contactFirstName": "Evan",
|
||||
"contactLastName": "Bacon",
|
||||
"contactPhone": "+1-555-555-5555",
|
||||
"contactEmail": "[email protected]",
|
||||
"demoAccountName": null,
|
||||
"demoAccountPassword": null,
|
||||
"demoAccountRequired": false,
|
||||
"notes": ""
|
||||
},
|
||||
"relationships": {
|
||||
"appStoreReviewAttachments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appStoreReviewAttachments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appStoreReviewAttachments"
|
||||
}
|
||||
},
|
||||
"appSandboxEntitlementUsages": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/relationships/appSandboxEntitlementUsages",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1/appSandboxEntitlementUsages"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreReviewDetails/APP_STORE_REVIEW_DETAILS_1"
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"data": [{
|
||||
"type": "appStoreVersionLocalizations",
|
||||
"id": "4bad54bc-3810-401a-a7f4-b02ef8206c41",
|
||||
"attributes": {
|
||||
"description": "Some of the best apps",
|
||||
"locale": "nl-NL",
|
||||
"keywords": "meme, red, square, x2",
|
||||
"marketingUrl": "https://github.com/bycedric/red-square-app",
|
||||
"promotionalText": null,
|
||||
"supportUrl": "https://github.com/bycedric/red-square-app/issues",
|
||||
"whatsNew": null
|
||||
},
|
||||
"relationships": {
|
||||
"appScreenshotSets": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/relationships/appScreenshotSets",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/appScreenshotSets"
|
||||
}
|
||||
},
|
||||
"appPreviewSets": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/relationships/appPreviewSets",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41/appPreviewSets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/4bad54bc-3810-401a-a7f4-b02ef8206c41"
|
||||
}
|
||||
}, {
|
||||
"type": "appStoreVersionLocalizations",
|
||||
"id": "61153520-c5df-40a1-bb2b-e09cbfc33fe9",
|
||||
"attributes": {
|
||||
"description": "Some of the best apps",
|
||||
"locale": "en-US",
|
||||
"keywords": "meme, red, square, x2",
|
||||
"marketingUrl": "https://github.com/bycedric/red-square-app",
|
||||
"promotionalText": null,
|
||||
"supportUrl": "https://github.com/bycedric/red-square-app/issues",
|
||||
"whatsNew": null
|
||||
},
|
||||
"relationships": {
|
||||
"appScreenshotSets": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/relationships/appScreenshotSets",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/appScreenshotSets"
|
||||
}
|
||||
},
|
||||
"appPreviewSets": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/relationships/appPreviewSets",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9/appPreviewSets"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersionLocalizations/61153520-c5df-40a1-bb2b-e09cbfc33fe9"
|
||||
}
|
||||
}],
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/949768f6-58e5-46b4-b046-23861f61831a/appStoreVersionLocalizations"
|
||||
},
|
||||
"meta": {
|
||||
"paging": {
|
||||
"total": 2,
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "appInfos",
|
||||
"id": "APP_INFO_1",
|
||||
"attributes": {
|
||||
"appStoreState": "PREPARE_FOR_SUBMISSION",
|
||||
"appStoreAgeRating": "NINE_PLUS",
|
||||
"brazilAgeRating": "TEN",
|
||||
"kidsAgeBand": null
|
||||
},
|
||||
"relationships": {
|
||||
"ageRatingDeclaration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/ageRatingDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/ageRatingDeclaration"
|
||||
}
|
||||
},
|
||||
"appInfoLocalizations": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/appInfoLocalizations",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/appInfoLocalizations"
|
||||
}
|
||||
},
|
||||
"primaryCategory": {
|
||||
"data": {
|
||||
"type": "appCategories",
|
||||
"id": "ENTERTAINMENT"
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primaryCategory",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primaryCategory"
|
||||
}
|
||||
},
|
||||
"primarySubcategoryOne": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryOne",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryOne"
|
||||
}
|
||||
},
|
||||
"primarySubcategoryTwo": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/primarySubcategoryTwo",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/primarySubcategoryTwo"
|
||||
}
|
||||
},
|
||||
"secondaryCategory": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondaryCategory",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondaryCategory"
|
||||
}
|
||||
},
|
||||
"secondarySubcategoryOne": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryOne",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryOne"
|
||||
}
|
||||
},
|
||||
"secondarySubcategoryTwo": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/relationships/secondarySubcategoryTwo",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1/secondarySubcategoryTwo"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appInfos/APP_INFO_1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"included": [
|
||||
{
|
||||
"type": "appCategories",
|
||||
"id": "ENTERTAINMENT",
|
||||
"attributes": {
|
||||
"platforms": [
|
||||
"IOS",
|
||||
"MAC_OS",
|
||||
"TV_OS"
|
||||
]
|
||||
},
|
||||
"relationships": {
|
||||
"subcategories": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appCategories/ENTERTAINMENT/relationships/subcategories",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appCategories/ENTERTAINMENT/subcategories"
|
||||
}
|
||||
},
|
||||
"parent": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appCategories/ENTERTAINMENT/relationships/parent",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appCategories/ENTERTAINMENT/parent"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appCategories/ENTERTAINMENT"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/apps/1611050397/appInfos?include=secondaryCategory%2CprimaryCategory%2CsecondarySubcategoryOne%2CprimarySubcategoryTwo%2CsecondarySubcategoryTwo%2CprimarySubcategoryOne"
|
||||
},
|
||||
"meta": {
|
||||
"paging": {
|
||||
"total": 1,
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"id": "APP_INFO_0",
|
||||
"status": "404",
|
||||
"code": "NOT_FOUND",
|
||||
"title": "The specified resource does not exist",
|
||||
"detail": "There is no resource of type 'appInfo' with id 'non-existing'"
|
||||
}
|
||||
]
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "appStoreVersions",
|
||||
"id": "APP_STORE_VERSION_1",
|
||||
"attributes": {
|
||||
"platform": "IOS",
|
||||
"versionString": "1.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_1/relationships/ageRatingDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/ageRatingDeclaration"
|
||||
}
|
||||
},
|
||||
"appStoreVersionLocalizations": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionLocalizations",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionLocalizations"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/build",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/build"
|
||||
}
|
||||
},
|
||||
"appStoreVersionPhasedRelease": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionPhasedRelease",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionPhasedRelease"
|
||||
}
|
||||
},
|
||||
"gameCenterConfiguration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/gameCenterConfiguration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/gameCenterConfiguration"
|
||||
}
|
||||
},
|
||||
"routingAppCoverage": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/routingAppCoverage",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/routingAppCoverage"
|
||||
}
|
||||
},
|
||||
"appStoreReviewDetail": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreReviewDetail",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreReviewDetail"
|
||||
}
|
||||
},
|
||||
"appStoreVersionSubmission": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionSubmission",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionSubmission"
|
||||
}
|
||||
},
|
||||
"resetRatingsRequest": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/resetRatingsRequest",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/resetRatingsRequest"
|
||||
}
|
||||
},
|
||||
"idfaDeclaration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/idfaDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/idfaDeclaration"
|
||||
}
|
||||
},
|
||||
"appClipDefaultExperience": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appClipDefaultExperience",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appClipDefaultExperience"
|
||||
}
|
||||
},
|
||||
"appStoreVersionStateChanges": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionStateChanges",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionStateChanges"
|
||||
}
|
||||
},
|
||||
"appStoreVersionExperiments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionExperiments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionExperiments"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "appStoreVersions",
|
||||
"id": "APP_STORE_VERSION_1",
|
||||
"attributes": {
|
||||
"platform": "IOS",
|
||||
"versionString": "1.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_1/relationships/ageRatingDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/ageRatingDeclaration"
|
||||
}
|
||||
},
|
||||
"appStoreVersionLocalizations": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionLocalizations",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionLocalizations"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/build",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/build"
|
||||
}
|
||||
},
|
||||
"appStoreVersionPhasedRelease": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionPhasedRelease",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionPhasedRelease"
|
||||
}
|
||||
},
|
||||
"gameCenterConfiguration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/gameCenterConfiguration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/gameCenterConfiguration"
|
||||
}
|
||||
},
|
||||
"routingAppCoverage": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/routingAppCoverage",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/routingAppCoverage"
|
||||
}
|
||||
},
|
||||
"appStoreReviewDetail": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreReviewDetail",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreReviewDetail"
|
||||
}
|
||||
},
|
||||
"appStoreVersionSubmission": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionSubmission",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionSubmission"
|
||||
}
|
||||
},
|
||||
"resetRatingsRequest": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/resetRatingsRequest",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/resetRatingsRequest"
|
||||
}
|
||||
},
|
||||
"idfaDeclaration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/idfaDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/idfaDeclaration"
|
||||
}
|
||||
},
|
||||
"appClipDefaultExperience": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appClipDefaultExperience",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appClipDefaultExperience"
|
||||
}
|
||||
},
|
||||
"appStoreVersionStateChanges": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionStateChanges",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionStateChanges"
|
||||
}
|
||||
},
|
||||
"appStoreVersionExperiments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionExperiments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionExperiments"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/apps/1611050397/appStoreVersions?include=appStoreVersionSubmission&filter%5Bplatform%5D=IOS&limit=2"
|
||||
},
|
||||
"meta": {
|
||||
"paging": {
|
||||
"total": 1,
|
||||
"limit": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"type": "appStoreVersions",
|
||||
"id": "APP_STORE_VERSION_1",
|
||||
"attributes": {
|
||||
"platform": "IOS",
|
||||
"versionString": "1.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_1/relationships/ageRatingDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/ageRatingDeclaration"
|
||||
}
|
||||
},
|
||||
"appStoreVersionLocalizations": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionLocalizations",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionLocalizations"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/build",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/build"
|
||||
}
|
||||
},
|
||||
"appStoreVersionPhasedRelease": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionPhasedRelease",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionPhasedRelease"
|
||||
}
|
||||
},
|
||||
"gameCenterConfiguration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/gameCenterConfiguration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/gameCenterConfiguration"
|
||||
}
|
||||
},
|
||||
"routingAppCoverage": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/routingAppCoverage",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/routingAppCoverage"
|
||||
}
|
||||
},
|
||||
"appStoreReviewDetail": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreReviewDetail",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreReviewDetail"
|
||||
}
|
||||
},
|
||||
"appStoreVersionSubmission": {
|
||||
"data": null,
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionSubmission",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionSubmission"
|
||||
}
|
||||
},
|
||||
"resetRatingsRequest": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/resetRatingsRequest",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/resetRatingsRequest"
|
||||
}
|
||||
},
|
||||
"idfaDeclaration": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/idfaDeclaration",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/idfaDeclaration"
|
||||
}
|
||||
},
|
||||
"appClipDefaultExperience": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appClipDefaultExperience",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appClipDefaultExperience"
|
||||
}
|
||||
},
|
||||
"appStoreVersionStateChanges": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionStateChanges",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionStateChanges"
|
||||
}
|
||||
},
|
||||
"appStoreVersionExperiments": {
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/relationships/appStoreVersionExperiments",
|
||||
"related": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1/appStoreVersionExperiments"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP_STORE_VERSION_1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "appStoreVersions",
|
||||
"id": "APP-STORE-VERSION-2",
|
||||
"attributes": {
|
||||
"platform": "IOS",
|
||||
"versionString": "1.1",
|
||||
"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-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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/appStoreVersions/APP-STORE-VERSION-2"
|
||||
}
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"self": "https://appstoreconnect.apple.com/iris/v1/apps/1611050397/appStoreVersions?include=appStoreVersionSubmission&filter%5Bplatform%5D=IOS&limit=2"
|
||||
},
|
||||
"meta": {
|
||||
"paging": {
|
||||
"total": 1,
|
||||
"limit": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/** A stubbed request context for the ASC API library */
|
||||
export const requestContext = {
|
||||
providerId: 1337,
|
||||
teamId: 'test-team-id',
|
||||
token: 'test-token',
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { AgeRatingDeclaration } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AgeRatingData = {
|
||||
/** The app age rating declaration for the app version */
|
||||
ageRating: AgeRatingDeclaration;
|
||||
};
|
||||
|
||||
export class AgeRatingTask extends AppleTask {
|
||||
public name = (): string => 'age rating declarations';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
assert(context.version, `App version information is not prepared, can't update age rating`);
|
||||
context.ageRating = (await context.version.getAgeRatingDeclarationAsync()) || undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (context.ageRating) {
|
||||
config.setAgeRating(context.ageRating.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
assert(context.ageRating, `Age rating not initialized, can't update age rating`);
|
||||
|
||||
const ageRating = config.getAgeRating();
|
||||
if (!ageRating) {
|
||||
Log.log(chalk`{dim - Skipped age rating update, no advisory configured}`);
|
||||
} else {
|
||||
context.ageRating = await logAsync(() => context.ageRating.updateAsync(ageRating), {
|
||||
pending: 'Updating age rating declaration...',
|
||||
success: 'Updated age rating declaration',
|
||||
failure: 'Failed to update age rating declaration',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { AppInfo, AppInfoLocalization } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { retryIfNullAsync } from '../../utils/retry';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AppInfoData = {
|
||||
/** The current app info that should be edited */
|
||||
info: AppInfo;
|
||||
/** All info locales that are enabled */
|
||||
infoLocales: AppInfoLocalization[];
|
||||
};
|
||||
|
||||
export class AppInfoTask extends AppleTask {
|
||||
public name = (): string => 'app information';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
const info = await retryIfNullAsync(() => context.app.getEditAppInfoAsync());
|
||||
assert(info, 'Could not resolve the editable app info to update');
|
||||
|
||||
context.info = info;
|
||||
context.infoLocales = await info.getLocalizationsAsync();
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
assert(context.info, `App info not initialized, can't download info`);
|
||||
|
||||
config.setCategories(context.info.attributes);
|
||||
|
||||
for (const locale of context.infoLocales) {
|
||||
config.setInfoLocale(locale.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
assert(context.info, `App info not initialized, can't update info`);
|
||||
|
||||
const categories = config.getCategories();
|
||||
if (!categories) {
|
||||
Log.log(chalk`{dim - Skipped app category update, not configured}`);
|
||||
} else {
|
||||
context.info = await logAsync(() => context.info.updateCategoriesAsync(categories), {
|
||||
pending: 'Updating app categories...',
|
||||
success: 'Updated app categories',
|
||||
failure: 'Failed updating app categories',
|
||||
});
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped localized info update, no locales configured}`);
|
||||
} else {
|
||||
// BUG: new issue introduced in ASC 1.8.0, when creating version locales, info locales are also generated
|
||||
context.infoLocales = await logAsync(() => context.info.getLocalizationsAsync(), {
|
||||
pending: 'Reloading localized info...',
|
||||
success: 'Reloaded localized info',
|
||||
failure: 'Failed reloading localized info',
|
||||
});
|
||||
|
||||
for (const locale of locales) {
|
||||
const attributes = config.getInfoLocale(locale);
|
||||
if (!attributes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const model = context.infoLocales.find(model => model.attributes.locale === locale);
|
||||
await logAsync(
|
||||
async () => {
|
||||
return model
|
||||
? await model.updateAsync(attributes)
|
||||
: await context.info.createLocalizationAsync({ ...attributes, locale });
|
||||
},
|
||||
{
|
||||
pending: `${model ? 'Updating' : 'Creating'} localized info for ${chalk.bold(
|
||||
locale
|
||||
)}...`,
|
||||
success: `${model ? 'Updated' : 'Created'} localized info for ${chalk.bold(locale)}`,
|
||||
failure: `Failed ${model ? 'updating' : 'creating'} localized info for ${chalk.bold(
|
||||
locale
|
||||
)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
context.infoLocales = await context.info.getLocalizationsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { AppStoreReviewDetail } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AppReviewData = {
|
||||
/** The current app info that should be edited */
|
||||
reviewDetail: AppStoreReviewDetail;
|
||||
};
|
||||
|
||||
/** Handle all contact, demo account, or instruction info that are required for the App Store review team. */
|
||||
export class AppReviewDetailTask extends AppleTask {
|
||||
public name = (): string => 'app review detail';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
assert(context.version, `App version not initialized, can't download store review details`);
|
||||
context.reviewDetail = (await context.version.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
if (context.reviewDetail) {
|
||||
config.setReviewDetails(context.reviewDetail.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadAsync({ config, context }: TaskUploadOptions): Promise<void> {
|
||||
const reviewDetail = config.getReviewDetails();
|
||||
if (!reviewDetail) {
|
||||
return Log.log(chalk`{dim - Skipped store review details, not configured}`);
|
||||
}
|
||||
|
||||
assert(context.version, `App version not initialized, can't upload store review details`);
|
||||
const { versionString } = context.version.attributes;
|
||||
|
||||
if (!context.reviewDetail) {
|
||||
// We can't set the demo required property when creating, omit it from the request
|
||||
const { demoAccountRequired, ...reviewDetailsToCreate } = reviewDetail;
|
||||
|
||||
context.reviewDetail = await logAsync(
|
||||
() => context.version.createReviewDetailAsync(reviewDetailsToCreate),
|
||||
{
|
||||
pending: `Creating store review details for ${chalk.bold(versionString)}...`,
|
||||
success: `Created store review details for ${chalk.bold(versionString)}`,
|
||||
failure: `Failed creating store review details for ${chalk.bold(versionString)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
context.reviewDetail = await logAsync(() => context.reviewDetail.updateAsync(reviewDetail), {
|
||||
pending: `Updating store review details for ${chalk.bold(versionString)}...`,
|
||||
success: `Updated store review details for ${chalk.bold(versionString)}`,
|
||||
failure: `Failed updating store review details for ${chalk.bold(versionString)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { App, AppStoreVersion, AppStoreVersionLocalization, Platform } from '@expo/apple-utils';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../../../log';
|
||||
import { logAsync } from '../../utils/log';
|
||||
import { retryIfNullAsync } from '../../utils/retry';
|
||||
import { AppleTask, TaskDownloadOptions, TaskPrepareOptions, TaskUploadOptions } from '../task';
|
||||
|
||||
export type AppVersionOptions = {
|
||||
/** If we should use the live version of the app (if available - defaults to false) */
|
||||
editLive: boolean;
|
||||
/** The platform to use (defaults to IOS) */
|
||||
platform: Platform;
|
||||
};
|
||||
|
||||
export type AppVersionData = {
|
||||
/** The current selected app store version to update */
|
||||
version: AppStoreVersion;
|
||||
/** If the current selected version is a live version, where not all properties are editable */
|
||||
versionIsLive: boolean;
|
||||
/** If the current selected version is the first version to be created */
|
||||
versionIsFirst: boolean;
|
||||
/** All version locales that should be, or are enabled */
|
||||
versionLocales: AppStoreVersionLocalization[];
|
||||
};
|
||||
|
||||
export class AppVersionTask extends AppleTask {
|
||||
private options: AppVersionOptions;
|
||||
|
||||
public constructor(options: Partial<AppVersionOptions> = {}) {
|
||||
super();
|
||||
this.options = {
|
||||
platform: options.platform ?? Platform.IOS,
|
||||
editLive: options.editLive ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
public name = (): string => (this.options.editLive ? 'live app version' : 'editable app version');
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
const { version, versionIsFirst, versionIsLive } = await resolveVersionAsync(
|
||||
context.app,
|
||||
this.options
|
||||
);
|
||||
|
||||
assert(version, 'Could not resolve a live or editable app version');
|
||||
|
||||
context.version = version;
|
||||
context.versionIsFirst = versionIsFirst;
|
||||
context.versionIsLive = versionIsLive;
|
||||
context.versionLocales = await version.getLocalizationsAsync();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
config.setVersionLocale(locale.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
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 (!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 ');
|
||||
|
||||
context.version = await logAsync(
|
||||
() => context.version.updateAsync({ ...version, ...release }),
|
||||
{
|
||||
pending: `Updating ${description} info for ${chalk.bold(versionString)}...`,
|
||||
success: `Updated ${description} info for ${chalk.bold(versionString)}`,
|
||||
failure: `Failed updating ${description} info for ${chalk.bold(versionString)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const locales = config.getLocales();
|
||||
if (locales.length <= 0) {
|
||||
Log.log(chalk`{dim - Skipped localized version update, no locales configured}`);
|
||||
} else {
|
||||
for (const locale of locales) {
|
||||
const attributes = config.getVersionLocale(locale, context);
|
||||
if (!attributes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const oldModel = context.versionLocales.find(model => model.attributes.locale === locale);
|
||||
await logAsync(
|
||||
async () => {
|
||||
return oldModel
|
||||
? await oldModel.updateAsync(attributes)
|
||||
: await context.version.createLocalizationAsync({ ...attributes, locale });
|
||||
},
|
||||
{
|
||||
pending: `${oldModel ? 'Updating' : 'Creating'} localized version for ${chalk.bold(
|
||||
locale
|
||||
)}...`,
|
||||
success: `${oldModel ? 'Updated' : 'Created'} localized version for ${chalk.bold(
|
||||
locale
|
||||
)}`,
|
||||
failure: `Failed ${
|
||||
oldModel ? 'updating' : 'creating'
|
||||
} localized version for ${chalk.bold(locale)}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
context.versionLocales = await context.version.getLocalizationsAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the AppStoreVersion instance, either from 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
|
||||
): Promise<{
|
||||
version: AppStoreVersion | null;
|
||||
versionIsLive: boolean;
|
||||
versionIsFirst: boolean;
|
||||
}> {
|
||||
let version: AppStoreVersion | null = null;
|
||||
let versionIsLive = false;
|
||||
|
||||
if (editLive) {
|
||||
version = await app.getLiveAppStoreVersionAsync({ platform });
|
||||
versionIsLive = !!version;
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
version = await retryIfNullAsync(() => app.getEditAppStoreVersionAsync({ platform }));
|
||||
}
|
||||
|
||||
const versions = await app.getAppStoreVersionsAsync({
|
||||
query: { limit: 2, filter: { platform } },
|
||||
});
|
||||
|
||||
return {
|
||||
version,
|
||||
versionIsLive,
|
||||
versionIsFirst: versions.length === 1,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { MetadataContext } from '../../context';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 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()];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { AgeRatingDeclarationProps } from '@expo/apple-utils';
|
||||
|
||||
export type AppleLocale = string;
|
||||
|
||||
export interface AppleMetadata {
|
||||
copyright?: string;
|
||||
info?: Record<AppleLocale, AppleInfo>;
|
||||
categories?: AppleCategory;
|
||||
release?: AppleRelease;
|
||||
advisory?: AppleAdvisory;
|
||||
preview?: Record<string, string[]>;
|
||||
review?: AppleReview;
|
||||
}
|
||||
|
||||
export type AppleAdvisory = Partial<AgeRatingDeclarationProps>;
|
||||
|
||||
/** Apps can define up to two categories, or categories with up to two subcategories */
|
||||
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;
|
||||
}
|
||||
|
||||
export interface AppleInfo {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/** Does not effect ASO https://developer.apple.com/app-store/product-page/ */
|
||||
promoText?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
releaseNotes?: string;
|
||||
marketingUrl?: string;
|
||||
privacyPolicyUrl?: string;
|
||||
privacyPolicyText?: string;
|
||||
privacyChoicesUrl?: string;
|
||||
supportUrl?: string;
|
||||
}
|
||||
|
||||
export interface AppleReview {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
phone: string;
|
||||
email: string;
|
||||
demoUsername?: string;
|
||||
demoPassword?: string;
|
||||
demoRequired?: boolean;
|
||||
notes?: string;
|
||||
// attachment?: string;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Ajv from 'ajv';
|
||||
import assert from 'assert';
|
||||
|
||||
import { AppleConfigReader } from './apple/config/reader';
|
||||
import { AppleConfigWriter } from './apple/config/writer';
|
||||
import { AppleMetadata } from './apple/types';
|
||||
|
||||
export interface MetadataConfig {
|
||||
/** The store configuration version */
|
||||
configVersion: number;
|
||||
/** All App Store related configuration */
|
||||
apple?: AppleMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the JSON Schema validation to normalize defaults and flag early config errors.
|
||||
* This includes validating the known store limitations for every configurable property.
|
||||
*/
|
||||
export function validateConfig(config: unknown): {
|
||||
valid: boolean;
|
||||
errors: Ajv.ErrorObject[];
|
||||
} {
|
||||
const validator = new Ajv({ allErrors: true, useDefaults: true })
|
||||
.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
|
||||
.compile(require('../../schema/metadata-0.json'));
|
||||
|
||||
const valid = validator(config) as boolean;
|
||||
|
||||
return { valid, errors: validator.errors || [] };
|
||||
}
|
||||
|
||||
/** Create a versioned deserializer to fetch App Store data from the store configuration. */
|
||||
export function createAppleReader(config: MetadataConfig): AppleConfigReader {
|
||||
assert(config.configVersion === 0, 'Unsupported store configuration version');
|
||||
assert(config.apple, 'No apple configuration found');
|
||||
return new AppleConfigReader(config.apple);
|
||||
}
|
||||
|
||||
/** Create the serializer to write the App Store to the store configuration. */
|
||||
export function createAppleWriter(): AppleConfigWriter {
|
||||
return new AppleConfigWriter();
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { App, Session } from '@expo/apple-utils';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonReader, SubmitProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { getRequestContext } from '../credentials/ios/appstore/authenticate';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { Actor } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
|
||||
export type MetadataContext = {
|
||||
/** Submission profile platform to use */
|
||||
platform: Platform.IOS;
|
||||
/** Resolved submission profile configuration */
|
||||
profile: SubmitProfile<Platform.IOS>;
|
||||
/** Configured store config path, relative from projectDir (defaults to store.config.json) */
|
||||
metadataPath: string;
|
||||
/** Authenticated Expo account */
|
||||
user: Actor;
|
||||
/** The store credentials manager */
|
||||
credentialsCtx: CredentialsContext;
|
||||
/** The app bundle identifier to use for the store */
|
||||
bundleIdentifier: string;
|
||||
/** Root of the Expo project directory */
|
||||
projectDir: string;
|
||||
/** Resolved Expo app configuration */
|
||||
exp: ExpoConfig;
|
||||
};
|
||||
|
||||
export type MetadataAppStoreAuthentication = {
|
||||
/** The root entity of the App store */
|
||||
app: App;
|
||||
/** The authentication state we used to fetch the root entity */
|
||||
auth: Partial<Session.AuthState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Metadata uses the submission profile to find the configured metadata filename.
|
||||
* Note, only iOS is supported for metadata right now.
|
||||
*/
|
||||
export async function createMetadataContextAsync(params: {
|
||||
projectDir: string;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp?: ExpoConfig;
|
||||
profileName?: string;
|
||||
}): Promise<MetadataContext> {
|
||||
const easJsonReader = new EasJsonReader(params.projectDir);
|
||||
const submitProfile = await easJsonReader.getSubmitProfileAsync(Platform.IOS, params.profileName);
|
||||
|
||||
const exp = params.exp ?? getExpoConfig(params.projectDir);
|
||||
const user = await ensureLoggedInAsync();
|
||||
const bundleIdentifier =
|
||||
submitProfile.bundleIdentifier ?? (await getBundleIdentifierAsync(params.projectDir, exp));
|
||||
|
||||
return {
|
||||
platform: Platform.IOS,
|
||||
profile: submitProfile,
|
||||
metadataPath: submitProfile.metadataPath ?? 'store.config.json',
|
||||
user,
|
||||
credentialsCtx: params.credentialsCtx,
|
||||
bundleIdentifier,
|
||||
projectDir: params.projectDir,
|
||||
exp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* To start syncing ASC entities, we need access to the apple utils App instance.
|
||||
* This ensures we are authenticated and have the app instance ready to use.
|
||||
*/
|
||||
export async function ensureMetadataAppStoreAuthenticatedAsync({
|
||||
credentialsCtx,
|
||||
bundleIdentifier,
|
||||
}: MetadataContext): Promise<MetadataAppStoreAuthentication> {
|
||||
const authCtx = await credentialsCtx.appStore.ensureAuthenticatedAsync();
|
||||
assert(authCtx.authState, 'Failed to authenticate with App Store Connect');
|
||||
|
||||
// TODO: improve error handling by mentioning possible configuration errors
|
||||
const app = await App.findAsync(getRequestContext(authCtx), { bundleId: bundleIdentifier });
|
||||
assert(app, `Failed to load app "${bundleIdentifier}" from App Store Connect`);
|
||||
|
||||
return { app, auth: authCtx.authState };
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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 { createAppleWriter } from './config';
|
||||
import { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { MetadataDownloadError, MetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
|
||||
/**
|
||||
* Generate a local store configuration from the stores.
|
||||
* 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 fileExists = await fs.pathExists(filePath);
|
||||
|
||||
if (fileExists) {
|
||||
const overwrite = await confirmAsync({
|
||||
message: `Do you want to overwrite the existing store configuration "${metadataCtx.metadataPath}"?`,
|
||||
});
|
||||
if (!overwrite) {
|
||||
throw new MetadataValidationError(`Store configuration already exists at "${filePath}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
|
||||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
|
||||
MetadataEvent.APPLE_METADATA_DOWNLOAD,
|
||||
{ app, auth }
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Downloading App Store configuration...');
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleWriter();
|
||||
const tasks = createAppleTasks(metadataCtx);
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task.prepareAsync({ context: taskCtx });
|
||||
} catch (error: any) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task.downloadAsync({ config, context: taskCtx as AppleData });
|
||||
} catch (error: any) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeJson(filePath, config.toSchema(), { spaces: 2 });
|
||||
} finally {
|
||||
unsubscribeTelemetry();
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new MetadataDownloadError(errors, executionId);
|
||||
}
|
||||
|
||||
return filePath;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { ErrorObject } from 'ajv';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log, { link } from '../log';
|
||||
|
||||
/**
|
||||
* Before syncing data to the ASC API, we need to validate the metadata config.
|
||||
* This error represents unrecoverable issues before syncing that data,
|
||||
* and should contain useful information for the user to solve before trying again.
|
||||
*/
|
||||
export class MetadataValidationError extends Error {
|
||||
public constructor(message?: string, public readonly errors: ErrorObject[] = []) {
|
||||
super(message ?? 'Store configuration validation failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a single entity failed to update, we don't block the other entities from uploading.
|
||||
* We still attempt to update the data in the stores as much as possible.
|
||||
* Because of that, we keep track of any errors encountered and throw this generic error.
|
||||
* It contains that list of encountered errors to present to the user.
|
||||
*/
|
||||
export class MetadataUploadError extends Error {
|
||||
public constructor(public readonly errors: Error[], public readonly executionId: string) {
|
||||
super(
|
||||
`Store configuration upload encountered ${
|
||||
errors.length === 1 ? 'an error' : `${errors.length} errors`
|
||||
}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If a single entity failed to download, we don't block the other entities from downloading.
|
||||
* We sill attempt to pull in the data from the stores as much as possible.
|
||||
* Because of that, we keep track of any errors envountered and throw this generic error.
|
||||
* It contains that list of encountered errors to present to the user.
|
||||
*/
|
||||
export class MetadataDownloadError extends Error {
|
||||
public constructor(public readonly errors: Error[], public readonly executionId: string) {
|
||||
super(
|
||||
`Store configuration download encountered ${
|
||||
errors.length === 1 ? 'an error' : `${errors.length} errors`
|
||||
}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the encountered metadata validation error in detail for the user.
|
||||
* This should help communicate any possible configuration error and help the user resolve it.
|
||||
*/
|
||||
export function logMetadataValidationError(error: MetadataValidationError): void {
|
||||
Log.newLine();
|
||||
Log.error(chalk.bold(error.message));
|
||||
if (error.errors?.length > 0) {
|
||||
// TODO(cedric): group errors by property to make multiple errors for same property more readable
|
||||
Log.log(error.errors.map(err => ` - ${chalk.bold(err.dataPath)} ${err.message}`).join('\n'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a thrown metadata error by informing the user what went wrong.
|
||||
* If a normal error is thrown, this method will re-throw that error to avoid consuming it.
|
||||
*/
|
||||
export function handleMetadataError(error: Error): void {
|
||||
if (error instanceof MetadataValidationError) {
|
||||
return logMetadataValidationError(error);
|
||||
}
|
||||
|
||||
if (error instanceof MetadataDownloadError || error instanceof MetadataUploadError) {
|
||||
Log.newLine();
|
||||
Log.error(chalk.bold(error.message));
|
||||
if (error.errors?.length > 0) {
|
||||
Log.newLine();
|
||||
Log.error(error.errors.map(err => err.message).join('\n\n'));
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log('Please check the logs for any configuration issues.');
|
||||
Log.log('If this issue persists, please open a new issue at:');
|
||||
// TODO: add execution ID to the issue template link
|
||||
Log.log(link('https://github.com/expo/eas-cli'));
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
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 { MetadataContext, ensureMetadataAppStoreAuthenticatedAsync } from './context';
|
||||
import { MetadataUploadError, MetadataValidationError, logMetadataValidationError } from './errors';
|
||||
import { subscribeTelemetry } from './utils/telemetry';
|
||||
|
||||
/**
|
||||
* Sync a local store configuration with the stores.
|
||||
* Note, only App Store is supported at this time.
|
||||
*/
|
||||
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 { app, auth } = await ensureMetadataAppStoreAuthenticatedAsync(metadataCtx);
|
||||
const { unsubscribeTelemetry, executionId } = subscribeTelemetry(
|
||||
MetadataEvent.APPLE_METADATA_UPLOAD,
|
||||
{ app, auth }
|
||||
);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Uploading App Store configuration...');
|
||||
|
||||
const errors: Error[] = [];
|
||||
const config = createAppleReader(fileData);
|
||||
const tasks = createAppleTasks(metadataCtx);
|
||||
const taskCtx = { app };
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task.prepareAsync({ context: taskCtx });
|
||||
} catch (error: any) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
for (const task of tasks) {
|
||||
try {
|
||||
await task.uploadAsync({ config, context: taskCtx as AppleData });
|
||||
} catch (error: any) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
unsubscribeTelemetry();
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new MetadataUploadError(errors, executionId);
|
||||
}
|
||||
|
||||
return { appleLink: `https://appstoreconnect.apple.com/apps/${app.id}/appstore` };
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { removeDatePrecision } from '../date';
|
||||
|
||||
describe(removeDatePrecision, () => {
|
||||
it('returns null for falsy values', () => {
|
||||
expect(removeDatePrecision(null)).toBeNull();
|
||||
expect(removeDatePrecision(undefined)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid dates', () => {
|
||||
expect(removeDatePrecision('invalid-date')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns date from date instance without time precision', () => {
|
||||
const input = new Date('2022-06-01T11:46:29.850Z');
|
||||
const output = new Date('2022-06-01T11:00:00.000Z');
|
||||
expect(removeDatePrecision(input)?.toISOString()).toBe(output.toISOString());
|
||||
});
|
||||
|
||||
it('returns date from string instance without time precision', () => {
|
||||
const input = '2022-06-01T11:46:29.850Z';
|
||||
const output = '2022-06-01T11:00:00.000Z';
|
||||
expect(removeDatePrecision(input)?.toISOString()).toBe(output);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { App } from '@expo/apple-utils';
|
||||
|
||||
import { TelemetryContext, makeDataScrubber } from '../telemetry';
|
||||
|
||||
describe(makeDataScrubber, () => {
|
||||
const stub: TelemetryContext = {
|
||||
// Only the App ID is considered to be sensitive
|
||||
app: new App({} as any, 'SECRET_APP_ID', {} as any),
|
||||
// Only the token properties and user credentials are considered to be sensitive
|
||||
auth: {
|
||||
username: 'SECRET_USERNAME',
|
||||
password: 'SECRET_PASSWORD',
|
||||
context: {
|
||||
token: 'SECRET_TOKEN',
|
||||
teamId: 'SECRET_TEAM_ID',
|
||||
providerId: 1337,
|
||||
},
|
||||
} as any,
|
||||
};
|
||||
|
||||
it('scrubs the app.id', () => {
|
||||
expect(makeDataScrubber(stub)('some text SECRET_APP_ID')).toBe('some text {APPLE_APP_ID}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.username', () => {
|
||||
expect(makeDataScrubber(stub)('some text SECRET_USERNAME')).toBe('some text {APPLE_USERNAME}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.password', () => {
|
||||
expect(makeDataScrubber(stub)('SECRET_PASSWORD')).toBe('{APPLE_PASSWORD}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.context.token', () => {
|
||||
expect(makeDataScrubber(stub)('some text SECRET_TOKEN')).toBe('some text {APPLE_TOKEN}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.context.token when using token instances', () => {
|
||||
const token = { getToken: () => 'SECRET_TOKEN_INSTANCE' } as any;
|
||||
const scrubber = makeDataScrubber({
|
||||
...stub,
|
||||
auth: {
|
||||
...stub.auth,
|
||||
context: {
|
||||
...stub.auth.context,
|
||||
token,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(scrubber('some text SECRET_TOKEN_INSTANCE')).toBe('some text {APPLE_TOKEN}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.context.teamId', () => {
|
||||
expect(makeDataScrubber(stub)('SECRET_TEAM_ID')).toBe('{APPLE_TEAM_ID}');
|
||||
});
|
||||
|
||||
it('scrubs the auth.context.teamId', () => {
|
||||
expect(makeDataScrubber(stub)('some provider 1337')).toBe('some provider {APPLE_PROVIDER_ID}');
|
||||
});
|
||||
|
||||
it('scrubber returns string representation of falsy values', () => {
|
||||
const scrubber = makeDataScrubber(stub);
|
||||
expect(scrubber(null)).toBe('null');
|
||||
expect(scrubber(undefined)).toBe('undefined');
|
||||
expect(scrubber(false)).toBe('false');
|
||||
});
|
||||
|
||||
it('scrubs multiple sensitive values at once', () => {
|
||||
expect(makeDataScrubber(stub)('SECRET_TOKEN SECRET_USERNAME SECRET_PASSWORD')).toBe(
|
||||
'{APPLE_TOKEN} {APPLE_USERNAME} {APPLE_PASSWORD}'
|
||||
);
|
||||
});
|
||||
|
||||
it('scrubs json and transforms it to string', () => {
|
||||
const scrubber = makeDataScrubber(stub);
|
||||
expect(scrubber({ foo: 'bar' })).toBe('{"foo":"bar"}');
|
||||
expect(scrubber({ value: 'SECRET_APP_ID' })).toBe('{"value":"{APPLE_APP_ID}"}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { ConnectModel } from '@expo/apple-utils';
|
||||
|
||||
/** Get the properties of a single App Store Connect entity */
|
||||
export type AttributesOf<T extends ConnectModel> = T['attributes'];
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Remove time precision from a date to avoid potential errors with the App Store.
|
||||
*
|
||||
* "status": "409",
|
||||
* "code": "ENTITY_ERROR.ATTRIBUTE.INVALID",
|
||||
* "title": "An attribute value is invalid.",
|
||||
* "detail": "The attribute 'earliestReleaseDate' only allows hour precision",
|
||||
* "source": {
|
||||
* "pointer": "/data/attributes/earliestReleaseDate"
|
||||
* }
|
||||
*/
|
||||
export function removeDatePrecision(date: null | undefined | string | number | Date): null | Date {
|
||||
if (date) {
|
||||
try {
|
||||
const result = new Date(date);
|
||||
result.setMinutes(0);
|
||||
result.setSeconds(0);
|
||||
result.setMilliseconds(0);
|
||||
|
||||
if (!isNaN(result.getTime())) {
|
||||
return result;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Ora, ora } from '../../ora';
|
||||
|
||||
type LogAsyncOptions = {
|
||||
/** If the spinner representing the async action should be hidden, e.g. for JSON output */
|
||||
hidden?: boolean;
|
||||
/** The message to display when the action is pending */
|
||||
pending: string;
|
||||
/** The message to display when the action succeeded */
|
||||
success: string;
|
||||
/** The message to display when the action failed */
|
||||
failure: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Log an asynchronous action using a spinner.
|
||||
*/
|
||||
export async function logAsync<T>(
|
||||
action: (spinner?: Ora) => Promise<T>,
|
||||
{ hidden, ...message }: LogAsyncOptions
|
||||
): Promise<T> {
|
||||
if (hidden) {
|
||||
return action();
|
||||
}
|
||||
|
||||
const spinner = ora(message.pending).start();
|
||||
try {
|
||||
const result = await action(spinner);
|
||||
spinner.succeed(message.success);
|
||||
return result;
|
||||
} catch (error) {
|
||||
spinner.fail(message.failure);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export async function waitAsync(duration: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, duration));
|
||||
}
|
||||
|
||||
type WithRetryOptions = {
|
||||
tries?: number;
|
||||
delay?: number;
|
||||
onRetry?: (triesLeft: number) => void;
|
||||
};
|
||||
|
||||
export async function retryIfNullAsync<T>(
|
||||
method: () => Promise<T | null>,
|
||||
options: WithRetryOptions = {}
|
||||
): Promise<T | null> {
|
||||
let { tries = 5, delay = 1000 } = options;
|
||||
|
||||
while (tries > 0) {
|
||||
const value = await method();
|
||||
if (value !== null) {
|
||||
return value;
|
||||
}
|
||||
tries--;
|
||||
options.onRetry?.(tries);
|
||||
await waitAsync(delay);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { App, Session, getRequestClient } from '@expo/apple-utils';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, MetadataEvent } from '../../analytics/events';
|
||||
|
||||
export type TelemetryContext = {
|
||||
app: App;
|
||||
auth: Partial<Session.AuthState>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscribe the telemetry to the ongoing metadata requests and responses.
|
||||
* When providing the app and auth info, we can scrub that data from the telemetry.
|
||||
* Returns an execution ID to group all events of a single run together, and a unsubscribe function.
|
||||
*/
|
||||
export function subscribeTelemetry(
|
||||
event: MetadataEvent,
|
||||
options: TelemetryContext
|
||||
): {
|
||||
/** Unsubscribe the telemetry from all apple-utils events */
|
||||
unsubscribeTelemetry: () => void;
|
||||
/** The unique id added to all telemetry events from a single execution */
|
||||
executionId: string;
|
||||
} {
|
||||
const executionId = uuidv4();
|
||||
const scrubber = makeDataScrubber(options);
|
||||
const { interceptors } = getRequestClient();
|
||||
|
||||
const responseInterceptorId = interceptors.response.use(
|
||||
response => {
|
||||
Analytics.logEvent(event, {
|
||||
executionId,
|
||||
type: 'response',
|
||||
phase: 'resolved',
|
||||
method: response.request.method.toUpperCase(),
|
||||
url: scrubber(response.request.path),
|
||||
status: String(response.status),
|
||||
statusText: scrubber(response.statusText),
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
Analytics.logEvent(event, {
|
||||
executionId,
|
||||
type: 'response',
|
||||
phase: 'rejected',
|
||||
method: error.request.method.toUpperCase(),
|
||||
url: scrubber(error.config.url),
|
||||
error: scrubber(error.message),
|
||||
status: String(error.response?.status),
|
||||
statusText: scrubber(error.response?.statusText),
|
||||
input: scrubber(error.config.data),
|
||||
output: scrubber(error.response?.data),
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
);
|
||||
|
||||
function unsubscribeTelemetry(): void {
|
||||
interceptors.response.eject(responseInterceptorId);
|
||||
}
|
||||
|
||||
return { unsubscribeTelemetry, executionId };
|
||||
}
|
||||
|
||||
/** Exposed for testing */
|
||||
export function makeDataScrubber({ app, auth }: TelemetryContext): <T>(data: T) => string {
|
||||
const token = getAuthTokenString(auth);
|
||||
const patterns: Record<string, RegExp | null> = {
|
||||
APPLE_APP_ID: new RegExp(app.id, 'gi'),
|
||||
APPLE_USERNAME: auth.username ? new RegExp(auth.username, 'gi') : null,
|
||||
APPLE_PASSWORD: auth.password ? new RegExp(auth.password, 'gi') : null,
|
||||
APPLE_TOKEN: token ? new RegExp(token, 'gi') : null,
|
||||
APPLE_TEAM_ID: auth.context?.teamId ? new RegExp(auth.context.teamId, 'gi') : null,
|
||||
APPLE_PROVIDER_ID: auth.context?.providerId
|
||||
? new RegExp(String(auth.context.providerId), 'gi')
|
||||
: null,
|
||||
};
|
||||
|
||||
const iterator = Object.entries(patterns);
|
||||
|
||||
return function scrubber(data) {
|
||||
if (!data) {
|
||||
return String(data);
|
||||
}
|
||||
|
||||
let value = typeof data === 'object' ? JSON.stringify(data) : String(data);
|
||||
for (const [replacement, pattern] of iterator) {
|
||||
if (pattern) {
|
||||
value = value.replace(pattern, `{${replacement}}`);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthTokenString(auth: TelemetryContext['auth']): string | null {
|
||||
if (!auth.context?.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof auth.context.token === 'object') {
|
||||
return auth.context.token.getToken();
|
||||
}
|
||||
|
||||
return auth.context.token;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import {
|
||||
MetadataJoi,
|
||||
TIMEOUT_LIMIT,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
@@ -412,38 +411,6 @@ describe(uploadAssetsAsync, () => {
|
||||
return { specifications: ['{}', '{}', '{}'] };
|
||||
});
|
||||
|
||||
// beforeEach(() => {
|
||||
// jest.useFakeTimers();
|
||||
// });
|
||||
it('throws an error if the upload exceeds TIMEOUT_LIMIT', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.DoesNotExist;
|
||||
mockdate.set(Date.now() + TIMEOUT_LIMIT + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).rejects.toThrow(
|
||||
'Asset upload timed out. Please try again.'
|
||||
);
|
||||
});
|
||||
it('resolves if the assets are already uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status = AssetMetadataStatus.Exists;
|
||||
@@ -468,7 +435,11 @@ describe(uploadAssetsAsync, () => {
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 0,
|
||||
});
|
||||
});
|
||||
it('resolves if the assets are eventually uploaded', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
@@ -496,6 +467,45 @@ describe(uploadAssetsAsync, () => {
|
||||
});
|
||||
|
||||
mockdate.set(0);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toBe(undefined);
|
||||
await expect(uploadAssetsAsync(assetsForUpdateInfoGroup)).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 2,
|
||||
});
|
||||
});
|
||||
it('updates spinner text throughout execution', async () => {
|
||||
jest.spyOn(PublishQuery, 'getAssetMetadataAsync').mockImplementation(async () => {
|
||||
const status =
|
||||
Date.now() === 0 ? AssetMetadataStatus.DoesNotExist : AssetMetadataStatus.Exists;
|
||||
mockdate.set(Date.now() + 1);
|
||||
jest.runAllTimers();
|
||||
return [
|
||||
{
|
||||
storageKey: 'qbgckgkgfdjnNuf9dQd7FDTWUmlEEzg7l1m1sKzQaq0',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // userDefinedAsset
|
||||
{
|
||||
storageKey: 'bbjgXFSIXtjviGwkaPFY0HG4dVVIGiXHAboRFQEqVa4',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // android.code
|
||||
{
|
||||
storageKey: 'dP-nC8EJXKz42XKh_Rc9tYxiGAT-ilpkRltEi6HIKeQ',
|
||||
status,
|
||||
__typename: 'AssetMetadataResult',
|
||||
}, // ios.code
|
||||
];
|
||||
});
|
||||
const updateSpinnerFn = jest.fn((_totalAssets, _missingAssets) => {});
|
||||
|
||||
mockdate.set(0);
|
||||
await uploadAssetsAsync(assetsForUpdateInfoGroup, updateSpinnerFn);
|
||||
const calls = updateSpinnerFn.mock.calls;
|
||||
expect(calls).toEqual([
|
||||
[3, 3],
|
||||
[3, 2],
|
||||
[3, 0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { readAppJson } from '../../build/utils/appJson';
|
||||
import env from '../../env';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
@@ -43,6 +44,9 @@ export async function getApplicationIdFromBareAsync(
|
||||
projectDir: string,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<string> {
|
||||
if (env.overrideAndroidApplicationId) {
|
||||
return env.overrideAndroidApplicationId;
|
||||
}
|
||||
const errorMessage = 'Could not read applicationId from Android project.';
|
||||
|
||||
if (gradleContext) {
|
||||
|
||||
@@ -35,7 +35,10 @@ export async function ensureProjectExistsAsync(projectInfo: ProjectInfo): Promis
|
||||
assert(account, `You must have access to the ${accountName} account to run this command`);
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl);
|
||||
const projectLink = terminalLink(projectFullName, projectDashboardUrl, {
|
||||
// https://github.com/sindresorhus/terminal-link/issues/18#issuecomment-1068020361
|
||||
fallback: () => `${projectFullName} (${projectDashboardUrl})`,
|
||||
});
|
||||
|
||||
const spinner = ora(`Linking to project ${chalk.bold(projectFullName)}`).start();
|
||||
|
||||
|
||||
@@ -5,18 +5,16 @@ import fs from 'fs-extra';
|
||||
import Joi from 'joi';
|
||||
import mime from 'mime';
|
||||
import path from 'path';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
||||
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 Log from '../log';
|
||||
import { uploadWithPresignedPostAsync } from '../uploads';
|
||||
import { expoCommandAsync } from '../utils/expoCli';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
|
||||
export const TIMEOUT_LIMIT = 60_000; // 1 minute
|
||||
|
||||
export type PublishPlatform = Extract<'android' | 'ios', Platform>;
|
||||
type Metadata = {
|
||||
version: number;
|
||||
@@ -157,11 +155,13 @@ export async function buildBundlesAsync({
|
||||
throw new Error('Could not locate package.json');
|
||||
}
|
||||
|
||||
await expoCommandAsync(
|
||||
projectDir,
|
||||
['export', '--output-dir', inputDir, '--experimental-bundle'],
|
||||
{ silent: !Log.isDebug }
|
||||
);
|
||||
await expoCommandAsync(projectDir, [
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--experimental-bundle',
|
||||
'--non-interactive',
|
||||
]);
|
||||
}
|
||||
|
||||
export async function resolveInputDirectoryAsync(customInputDirectory: string): Promise<string> {
|
||||
@@ -245,7 +245,16 @@ export async function filterOutAssetsThatAlreadyExistAsync(
|
||||
return missingAssets;
|
||||
}
|
||||
|
||||
export async function uploadAssetsAsync(assetsForUpdateInfoGroup: CollectedAssets): Promise<void> {
|
||||
type AssetUploadResult = {
|
||||
assetCount: number;
|
||||
uniqueAssetCount: number;
|
||||
uniqueUploadedAssetCount: number;
|
||||
};
|
||||
|
||||
export async function uploadAssetsAsync(
|
||||
assetsForUpdateInfoGroup: CollectedAssets,
|
||||
updateSpinnerText?: (totalAssets: number, missingAssets: number) => void
|
||||
): Promise<AssetUploadResult> {
|
||||
let assets: RawAsset[] = [];
|
||||
let platform: keyof CollectedAssets;
|
||||
for (platform in assetsForUpdateInfoGroup) {
|
||||
@@ -270,29 +279,42 @@ export async function uploadAssetsAsync(assetsForUpdateInfoGroup: CollectedAsset
|
||||
}
|
||||
>(assetsWithStorageKey, asset => asset.storageKey);
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
updateSpinnerText?.(totalAssets, missingAssets.length);
|
||||
|
||||
const assetUploadPromiseLimit = promiseLimit(15);
|
||||
|
||||
await Promise.all(
|
||||
missingAssets.map((missingAsset, i) => {
|
||||
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
|
||||
return uploadWithPresignedPostAsync(missingAsset.path, presignedPost);
|
||||
assetUploadPromiseLimit(async () => {
|
||||
const presignedPost: PresignedPost = JSON.parse(specifications[i]);
|
||||
await uploadWithPresignedPostAsync(missingAsset.path, presignedPost);
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
// Wait up to TIMEOUT_LIMIT for assets to be uploaded and processed
|
||||
const start = Date.now();
|
||||
let timeout = 1;
|
||||
while (missingAssets.length > 0) {
|
||||
const timeoutPromise = new Promise(resolve => setTimeout(resolve, timeout * 1000)); // linear backoff
|
||||
const timeoutPromise = new Promise(resolve =>
|
||||
setTimeout(resolve, Math.min(timeout * 1000, 5000))
|
||||
); // linear backoff
|
||||
missingAssets = await filterOutAssetsThatAlreadyExistAsync(missingAssets);
|
||||
await timeoutPromise; // await after filterOutAssetsThatAlreadyExistAsync for easy mocking with jest.runAllTimers
|
||||
timeout += 1;
|
||||
|
||||
if (Date.now() - start > TIMEOUT_LIMIT) {
|
||||
throw new Error('Asset upload timed out. Please try again.');
|
||||
}
|
||||
updateSpinnerText?.(totalAssets, missingAssets.length);
|
||||
}
|
||||
return {
|
||||
assetCount: assets.length,
|
||||
uniqueAssetCount: uniqueAssets.length,
|
||||
uniqueUploadedAssetCount,
|
||||
};
|
||||
}
|
||||
@@ -9,23 +9,26 @@ export async function resolveWorkflowAsync(
|
||||
projectDir: string,
|
||||
platform: Platform
|
||||
): Promise<Workflow> {
|
||||
let platformWorkflowMarker;
|
||||
let platformWorkflowMarkers: string[];
|
||||
try {
|
||||
platformWorkflowMarker =
|
||||
platformWorkflowMarkers =
|
||||
platform === Platform.ANDROID
|
||||
? await AndroidConfig.Paths.getAndroidManifestAsync(projectDir)
|
||||
: IOSConfig.Paths.getPBXProjectPath(projectDir);
|
||||
? [
|
||||
path.join(projectDir, 'android/app/build.gradle'),
|
||||
await AndroidConfig.Paths.getAndroidManifestAsync(projectDir),
|
||||
]
|
||||
: [IOSConfig.Paths.getPBXProjectPath(projectDir)];
|
||||
} catch {
|
||||
return Workflow.MANAGED;
|
||||
}
|
||||
|
||||
if (await fs.pathExists(platformWorkflowMarker)) {
|
||||
return (await getVcsClient().isFileIgnoredAsync(
|
||||
path.relative(projectDir, platformWorkflowMarker)
|
||||
))
|
||||
? Workflow.MANAGED
|
||||
: Workflow.GENERIC;
|
||||
} else {
|
||||
return Workflow.MANAGED;
|
||||
for (const marker of platformWorkflowMarkers) {
|
||||
if (
|
||||
(await fs.pathExists(marker)) &&
|
||||
!(await getVcsClient().isFileIgnoredAsync(path.relative(projectDir, marker)))
|
||||
) {
|
||||
return Workflow.GENERIC;
|
||||
}
|
||||
}
|
||||
return Workflow.MANAGED;
|
||||
}
|
||||
@@ -242,6 +242,7 @@ describe(getArchiveAsync, () => {
|
||||
it('handles path source', async () => {
|
||||
const path = '/archive.apk';
|
||||
await fs.writeFile(path, 'some content');
|
||||
|
||||
jest.mocked(uploadAsync).mockResolvedValueOnce({ url: ARCHIVE_URL, bucketKey: 'wat' });
|
||||
|
||||
const archive = await getArchiveAsync({
|
||||
|
||||
@@ -78,7 +78,7 @@ function printInstructionsForIosSubmission(submission: SubmissionFragment): void
|
||||
'- It usually takes about 5-10 minutes depending on how busy Apple servers are.',
|
||||
// ascAppIdentifier should be always available for ios submissions but check it anyway
|
||||
submission.iosConfig?.ascAppIdentifier &&
|
||||
`- When it’s done, you can see your build here: ${link(
|
||||
`- When it's done, you can see your build here: ${link(
|
||||
`https://appstoreconnect.apple.com/apps/${submission.iosConfig?.ascAppIdentifier}/appstore/ios`
|
||||
)}`,
|
||||
].join('\n');
|
||||
|
||||
@@ -6,6 +6,7 @@ enum SubmissionErrorCode {
|
||||
ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_DOWNLOAD_FORBIDDEN_ERROR',
|
||||
ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_NO_FILES_FOUND_ERROR',
|
||||
ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR = 'SUBMISSION_SERVICE_COMMON_ARCHIVE_EXTRACT_CORRUPT_ARCHIVE_ERROR',
|
||||
UPLOAD_TAKING_TOO_LONG_ERROR = 'SUBMISSION_SERVICE_COMMON_UPLOAD_TAKING_TOO_LONG_ERROR',
|
||||
ANDROID_UNKNOWN_ERROR = 'SUBMISSION_SERVICE_ANDROID_UNKNOWN_ERROR',
|
||||
ANDROID_FIRST_UPLOAD_ERROR = 'SUBMISSION_SERVICE_ANDROID_FIRST_UPLOAD_ERROR',
|
||||
ANDROID_OLD_VERSION_CODE_ERROR = 'SUBMISSION_SERVICE_ANDROID_OLD_VERSION_CODE_ERROR',
|
||||
@@ -70,6 +71,8 @@ const SubmissionErrorMessages: Record<SubmissionErrorCode, string> = {
|
||||
'Invalid Provisioning Profile Signature (ITMS-90165)\n' +
|
||||
"Some of Apple's certificates have expired.\n" +
|
||||
'Please delete your Provisioning Profile from your account. Then rebuild the app interactively to generate a new one, and try submitting it to the App Store again.',
|
||||
[SubmissionErrorCode.UPLOAD_TAKING_TOO_LONG_ERROR]:
|
||||
'Submission has reached the timeout limit. Please try again.',
|
||||
};
|
||||
|
||||
export function printSubmissionError(error: SubmissionError): boolean {
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import {
|
||||
PublishPlatformFlag,
|
||||
ensureBranchExistsAsync,
|
||||
getUpdatesToRepublishInteractiveAsync,
|
||||
truncatePublishUpdateMessage,
|
||||
} from '../../commands/update';
|
||||
import { graphqlClient } from '../../graphql/client';
|
||||
import { ViewBranchUpdatesQuery } from '../../graphql/generated';
|
||||
import { viewBranchUpdatesQueryUpdateLimit } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { selectAsync } from '../../prompts';
|
||||
|
||||
const appId = '6c94sxe6-37d2-4700-52fa-1b813204dad2';
|
||||
const branchId = '5e84e3cb-563e-4022-a65e-6e7a42fe4ed3';
|
||||
const appName = '@tester/test';
|
||||
const branchName = 'test-branch';
|
||||
const projectName = 'test-project';
|
||||
|
||||
jest.mock('../../graphql/queries/UpdateQuery', () => {
|
||||
const actual = jest.requireActual('../../graphql/queries/UpdateQuery');
|
||||
return {
|
||||
...actual,
|
||||
getViewBranchUpdatesQueryUpdateLimit: jest.fn(actual.getViewBranchUpdatesQueryUpdateLimit),
|
||||
};
|
||||
});
|
||||
jest.mock('../../prompts', () => ({
|
||||
selectAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../commands/update', () => {
|
||||
const actual = jest.requireActual('../../commands/update');
|
||||
return {
|
||||
...actual,
|
||||
ensureBranchExistsAsync: jest.fn(actual.ensureBranchExistsAsync),
|
||||
getUpdatesToRepublishInteractiveAsync: jest.fn(actual.getUpdatesToRepublishInteractiveAsync),
|
||||
};
|
||||
});
|
||||
const updatesToResolve = jest.fn(
|
||||
(): Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'] => []
|
||||
);
|
||||
jest.mock('../../graphql/client', () => ({
|
||||
...jest.requireActual('../../graphql/client'),
|
||||
graphqlClient: {
|
||||
query: jest.fn(() => ({
|
||||
toPromise: () =>
|
||||
Promise.resolve({
|
||||
data: {
|
||||
__typename: 'RootQuery',
|
||||
app: {
|
||||
__typename: 'AppQuery',
|
||||
byId: {
|
||||
__typename: 'App',
|
||||
id: appId,
|
||||
updateBranchByName: {
|
||||
__typename: 'UpdateBranch',
|
||||
id: branchId,
|
||||
name: branchName,
|
||||
updates: updatesToResolve(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})),
|
||||
mutation: jest.fn(() => ({
|
||||
toPromise: () => ({
|
||||
error: {
|
||||
graphQLErrors: [
|
||||
{
|
||||
GraphQLError: `A channel already exists with (app_id, name) = (${appId}, ${appName})`,
|
||||
extensions: {
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
errorCode: 'CHANNEL_ALREADY_EXISTS',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
function createMockUpdates(
|
||||
{
|
||||
updateCount = 1,
|
||||
platformFlag = 'all',
|
||||
groupId,
|
||||
}: Partial<{
|
||||
updateCount: number;
|
||||
platformFlag: PublishPlatformFlag;
|
||||
groupId: string;
|
||||
}> = {
|
||||
updateCount: 1,
|
||||
platformFlag: 'all',
|
||||
}
|
||||
): Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'] {
|
||||
let updates: Exclude<
|
||||
Exclude<ViewBranchUpdatesQuery['app'], null | undefined>['byId']['updateBranchByName'],
|
||||
null | undefined
|
||||
>['updates'] = [];
|
||||
if (platformFlag === 'all') {
|
||||
updateCount = Math.ceil(updateCount / 2);
|
||||
}
|
||||
for (let i = 0; i < updateCount; i++) {
|
||||
const androidUpdate = {
|
||||
id: uuidv4(),
|
||||
group: uuidv4(),
|
||||
message: 'default test message',
|
||||
createdAt: new Date(),
|
||||
runtimeVersion: 'exposdk:44.0.0',
|
||||
platform: 'android',
|
||||
manifestFragment: '',
|
||||
};
|
||||
const iosUpdate = {
|
||||
...androidUpdate,
|
||||
id: uuidv4(),
|
||||
platform: 'ios',
|
||||
};
|
||||
if (!i && groupId) {
|
||||
androidUpdate.group = groupId;
|
||||
iosUpdate.group = groupId;
|
||||
}
|
||||
const isOddNumberedUpdateCount = !(updateCount % 2);
|
||||
const isFinalLoop = i === updateCount - 1;
|
||||
switch (platformFlag) {
|
||||
case 'all':
|
||||
updates = [
|
||||
...updates,
|
||||
androidUpdate,
|
||||
...(isFinalLoop && isOddNumberedUpdateCount ? [] : [iosUpdate]),
|
||||
];
|
||||
break;
|
||||
case 'android':
|
||||
updates = [...updates, androidUpdate];
|
||||
break;
|
||||
case 'ios':
|
||||
updates = [...updates, iosUpdate];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return updates;
|
||||
}
|
||||
|
||||
describe('UpdatePublish', () => {
|
||||
describe('ensureBranchExistsAsync', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(graphqlClient.query).mockClear();
|
||||
jest.mocked(graphqlClient.mutation).mockClear();
|
||||
});
|
||||
|
||||
it('defaults limit to viewBranchUpdatesQueryUpdateLimit and offset to 0 when none are provided', async () => {
|
||||
await ensureBranchExistsAsync({ appId, name: appName });
|
||||
const [[, bindings]] = jest.mocked(graphqlClient.query).mock.calls;
|
||||
|
||||
expect(bindings).toEqual({
|
||||
appId,
|
||||
name: appName,
|
||||
limit: viewBranchUpdatesQueryUpdateLimit,
|
||||
offset: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
[0, 0],
|
||||
[10, 50],
|
||||
[100, 3],
|
||||
[1000, 77],
|
||||
])('sets the limit to %s and offset to %s when provided', async (limit, offset) => {
|
||||
await ensureBranchExistsAsync({ appId, name: appName, limit, offset });
|
||||
const [[, bindings]] = jest.mocked(graphqlClient.query).mock.calls;
|
||||
|
||||
expect(bindings).toEqual({
|
||||
appId,
|
||||
name: appName,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUpdatesToRepublishInteractiveAsync', () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(graphqlClient.query).mockClear();
|
||||
jest.mocked(graphqlClient.mutation).mockClear();
|
||||
jest.mocked(getUpdatesToRepublishInteractiveAsync).mockClear();
|
||||
jest.mocked(selectAsync).mockClear();
|
||||
});
|
||||
|
||||
it('throws when there are no updates', async () => {
|
||||
const platformFlag = 'all';
|
||||
await expect(
|
||||
async () =>
|
||||
await getUpdatesToRepublishInteractiveAsync(projectName, branchName, platformFlag, 50)
|
||||
).rejects.toThrow(
|
||||
`There are no updates on branch "${branchName}" published for the platform(s) ${platformFlag}. Did you mean to publish a new update instead?`
|
||||
);
|
||||
});
|
||||
|
||||
it('fetches multiple pages of updates and can select an update from the most recent page', async () => {
|
||||
const mockSelectedGroupId = uuidv4();
|
||||
const pageSize = 2;
|
||||
updatesToResolve
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize + 1 }))
|
||||
.mockReturnValueOnce(
|
||||
createMockUpdates({ updateCount: pageSize + 1, groupId: mockSelectedGroupId })
|
||||
);
|
||||
jest
|
||||
.mocked(selectAsync)
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce(mockSelectedGroupId);
|
||||
|
||||
const selectedUpdates = await getUpdatesToRepublishInteractiveAsync(
|
||||
projectName,
|
||||
branchName,
|
||||
'all',
|
||||
pageSize
|
||||
);
|
||||
expect(selectedUpdates.every(update => update.group === mockSelectedGroupId)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('fetches multiple pages of updates and can return an update from a previous page', async () => {
|
||||
const mockSelectedGroupId = uuidv4();
|
||||
const pageSize = 5;
|
||||
updatesToResolve
|
||||
.mockReturnValueOnce(
|
||||
createMockUpdates({ updateCount: pageSize + 1, groupId: mockSelectedGroupId })
|
||||
)
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize + 1 }));
|
||||
jest
|
||||
.mocked(selectAsync)
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce(mockSelectedGroupId);
|
||||
|
||||
const selectedUpdates = await getUpdatesToRepublishInteractiveAsync(
|
||||
projectName,
|
||||
branchName,
|
||||
'all',
|
||||
pageSize
|
||||
);
|
||||
expect(selectedUpdates.every(update => update.group === mockSelectedGroupId)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('displays the option to fetch more pages as long as there are unfetched updates left', async () => {
|
||||
const mockSelectedGroupId = uuidv4();
|
||||
const pageSize = 10;
|
||||
updatesToResolve
|
||||
.mockReturnValueOnce(
|
||||
createMockUpdates({ updateCount: pageSize + 1, groupId: mockSelectedGroupId })
|
||||
)
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize + 1 }))
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize }));
|
||||
jest
|
||||
.mocked(selectAsync)
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce(mockSelectedGroupId);
|
||||
|
||||
await getUpdatesToRepublishInteractiveAsync(projectName, branchName, 'all', pageSize);
|
||||
const { calls } = jest.mocked(selectAsync).mock;
|
||||
expect(calls.length).toEqual(3);
|
||||
const [[, firstOptions], [, secondOptions], [, thirdOptions]] = calls;
|
||||
const fetchMoreValue = '_fetchMore';
|
||||
expect(firstOptions[firstOptions.length - 1].value).toEqual(fetchMoreValue);
|
||||
expect(firstOptions.length).toEqual(pageSize / 2 + 1); // + 1 === fetch more object
|
||||
expect(secondOptions[secondOptions.length - 1].value).toEqual(fetchMoreValue);
|
||||
expect(secondOptions.length).toEqual((pageSize / 2) * 2 + 1);
|
||||
expect(thirdOptions.every(update => update.value !== fetchMoreValue)).toBeTruthy();
|
||||
expect(thirdOptions.length).toEqual((pageSize / 2) * 3);
|
||||
});
|
||||
|
||||
it('paginates update requests as expected', async () => {
|
||||
const mockSelectedGroupId = uuidv4();
|
||||
const pageSize = 50;
|
||||
updatesToResolve
|
||||
.mockReturnValueOnce(
|
||||
createMockUpdates({ updateCount: pageSize + 1, groupId: mockSelectedGroupId })
|
||||
)
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize + 1 }))
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize + 1 }))
|
||||
.mockReturnValueOnce(createMockUpdates({ updateCount: pageSize }));
|
||||
jest
|
||||
.mocked(selectAsync)
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce('_fetchMore')
|
||||
.mockResolvedValueOnce(mockSelectedGroupId);
|
||||
|
||||
await getUpdatesToRepublishInteractiveAsync(projectName, branchName, 'all', pageSize);
|
||||
const gqlBindings = jest.mocked(graphqlClient.query).mock.calls.map(call => call[1]);
|
||||
expect(gqlBindings).toEqual([
|
||||
expect.objectContaining({ limit: pageSize + 1, offset: pageSize * 0 }),
|
||||
expect.objectContaining({ limit: pageSize + 1, offset: pageSize * 1 }),
|
||||
expect.objectContaining({ limit: pageSize + 1, offset: pageSize * 2 }),
|
||||
expect.objectContaining({ limit: pageSize + 1, offset: pageSize * 3 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(truncatePublishUpdateMessage.name, () => {
|
||||
const warnSpy = jest.spyOn(Log, 'warn');
|
||||
beforeEach(() => {
|
||||
warnSpy.mockClear();
|
||||
});
|
||||
|
||||
it('does not alter messages with less than 1024 characters', () => {
|
||||
const message = 'Small message =)';
|
||||
const truncatedMessage = truncatePublishUpdateMessage(message);
|
||||
expect(truncatedMessage).toEqual(message);
|
||||
expect(warnSpy).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('truncates messages to a length of 1024, including ellipses', () => {
|
||||
const longMessage = Array.from({ length: 2024 }, () => 'a').join('');
|
||||
const truncatedMessage = truncatePublishUpdateMessage(longMessage);
|
||||
expect(truncatedMessage.length).toEqual(1024);
|
||||
expect(truncatedMessage.slice(-3)).toEqual('...');
|
||||
expect(warnSpy).toBeCalledWith(
|
||||
'Update message exceeds the allowed 1024 character limit. Truncating message...'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import assert from 'assert';
|
||||
import FormData from 'form-data';
|
||||
import fs from 'fs-extra';
|
||||
import { Response } from 'node-fetch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import { URL } from 'url';
|
||||
|
||||
import fetch from './fetch';
|
||||
import { UploadSessionType } from './graphql/generated';
|
||||
@@ -13,8 +16,14 @@ export async function uploadAsync(
|
||||
handleProgressEvent: ProgressHandler
|
||||
): Promise<{ url: string; bucketKey: string }> {
|
||||
const presignedPost = await UploadSessionMutation.createUploadSessionAsync(type);
|
||||
const url = await uploadWithPresignedPostAsync(path, presignedPost, handleProgressEvent);
|
||||
assert(presignedPost.fields.key, 'key is not specified in in presigned post');
|
||||
|
||||
const response = await uploadWithPresignedPostAsync(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)`
|
||||
);
|
||||
const url = fixS3Url(location);
|
||||
return { url, bucketKey: presignedPost.fields.key };
|
||||
}
|
||||
|
||||
@@ -22,7 +31,7 @@ export async function uploadWithPresignedPostAsync(
|
||||
file: string,
|
||||
presignedPost: PresignedPost,
|
||||
handleProgressEvent?: ProgressHandler
|
||||
): Promise<string> {
|
||||
): Promise<Response> {
|
||||
const fileStat = await fs.stat(file);
|
||||
const fileSize = fileStat.size;
|
||||
const form = new FormData();
|
||||
@@ -54,13 +63,23 @@ export async function uploadWithPresignedPostAsync(
|
||||
try {
|
||||
const response = await uploadPromise;
|
||||
handleProgressEvent({ isComplete: true });
|
||||
return String(response.headers.get('location'));
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
handleProgressEvent({ isComplete: true, error });
|
||||
throw error;
|
||||
}
|
||||
} else {
|
||||
const response = await uploadPromise;
|
||||
return String(response.headers.get('location'));
|
||||
return await uploadPromise;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* S3 returns broken URLs, sth like:
|
||||
* https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c
|
||||
* This function replaces %2F with /.
|
||||
*/
|
||||
export function fixS3Url(archiveUrl: string): string {
|
||||
const parsed = new URL(archiveUrl);
|
||||
parsed.pathname = decodeURIComponent(parsed.pathname);
|
||||
return parsed.toString();
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJsonReader, errors } from '@expo/eas-json';
|
||||
import { EasJsonReader, errors } from '@expo/eas-json';
|
||||
|
||||
import { selectAsync } from '../../prompts';
|
||||
import { getProfilesAsync } from '../profiles';
|
||||
@@ -31,7 +31,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
it('defaults to production profile', async () => {
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
easJsonReader: new EasJsonReader('/fake'),
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
@@ -39,53 +39,8 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
expect(result[0].profileName).toBe('production');
|
||||
expect(result[1].profileName).toBe('production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
});
|
||||
|
||||
it('defaults to release profile when production profile is non-existent', async () => {
|
||||
getBuildProfileAsync
|
||||
.mockImplementationOnce(() => {
|
||||
throw new errors.MissingProfileError();
|
||||
})
|
||||
.mockImplementationOnce(() => {
|
||||
throw new errors.MissingProfileError();
|
||||
});
|
||||
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
});
|
||||
|
||||
expect(result[0].profileName).toBe('release');
|
||||
expect(result[1].profileName).toBe('release');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'production');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'release');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, 'release');
|
||||
});
|
||||
|
||||
it('asks the user to pick profile if "release" does not exist', async () => {
|
||||
getBuildProfileAsync.mockImplementation(async (_, profileName) => {
|
||||
if (profileName === 'foo') {
|
||||
return {} as unknown as BuildProfile<Platform>;
|
||||
}
|
||||
throw new errors.MissingProfileError();
|
||||
});
|
||||
getBuildProfileNamesAsync.mockImplementation(() => Promise.resolve(['foo', 'bar']));
|
||||
jest.mocked(selectAsync).mockImplementation(async () => 'foo');
|
||||
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
});
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, 'foo');
|
||||
expect(result[0].profileName).toBe('foo');
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.ANDROID, undefined);
|
||||
expect(getBuildProfileAsync).toBeCalledWith(Platform.IOS, undefined);
|
||||
});
|
||||
|
||||
it('throws an error if there are no profiles in eas.json', async () => {
|
||||
@@ -96,7 +51,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
easJsonReader: new EasJsonReader('/fake'),
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
@@ -106,7 +61,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
it('gets a specific profile', async () => {
|
||||
const result = await getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
easJsonReader: new EasJsonReader('/fake'),
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: 'custom-profile',
|
||||
type: 'build',
|
||||
@@ -125,7 +80,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
projectDir: '/fake',
|
||||
easJsonReader: new EasJsonReader('/fake'),
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import uniq from '../uniq';
|
||||
|
||||
describe(uniq, () => {
|
||||
it('returns unique numbers from a list', () => {
|
||||
expect(uniq([1, 2, 2, 3, 4, 2, 3])).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('returns unique strings from a list', () => {
|
||||
expect(uniq(['hi', 'hello', 'hello', 'ola'])).toEqual(['hi', 'hello', 'ola']);
|
||||
});
|
||||
|
||||
it('returns unique mixed types from a list', () => {
|
||||
expect(uniq([1, 2, 2, 'hi', 'hi', 'hello', 3])).toEqual([1, 2, 'hi', 'hello', 3]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export default function uniq<T = any>(items: T[]): T[] {
|
||||
const set = new Set(items);
|
||||
return [...set];
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import {
|
||||
BuildProfile,
|
||||
EasJsonReader,
|
||||
ProfileType,
|
||||
SubmitProfile,
|
||||
errors,
|
||||
getDefaultSubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import Log from '../log';
|
||||
import { ExpoChoice, selectAsync } from '../prompts';
|
||||
import { BuildProfile, EasJsonReader, ProfileType, SubmitProfile } from '@expo/eas-json';
|
||||
|
||||
type EasProfile<T extends ProfileType> = T extends 'build'
|
||||
? BuildProfile<Platform>
|
||||
@@ -23,109 +12,26 @@ export type ProfileData<T extends ProfileType> = {
|
||||
};
|
||||
|
||||
export async function getProfilesAsync<T extends ProfileType>({
|
||||
projectDir,
|
||||
easJsonReader,
|
||||
platforms,
|
||||
profileName,
|
||||
type,
|
||||
}: {
|
||||
projectDir: string;
|
||||
easJsonReader: EasJsonReader;
|
||||
platforms: Platform[];
|
||||
profileName?: string;
|
||||
type: T;
|
||||
}): Promise<ProfileData<T>[]> {
|
||||
const results = platforms.map(async function (platform) {
|
||||
if (profileName) {
|
||||
const profile = await readProfileAsync({ projectDir, platform, type, profileName });
|
||||
return {
|
||||
profile,
|
||||
profileName,
|
||||
platform,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName: 'production',
|
||||
});
|
||||
return {
|
||||
profile,
|
||||
profileName: 'production',
|
||||
platform,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof errors.MissingProfileError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
platform,
|
||||
type,
|
||||
profileName: 'release',
|
||||
});
|
||||
Log.warn(
|
||||
`The default profile changed from ${chalk.bold('release')} to ${chalk.bold(
|
||||
'production'
|
||||
)}. We detected that you still have a ${chalk.bold(
|
||||
'release'
|
||||
)} build profile, so we are using it. Update eas.json to have a profile named ${chalk.bold(
|
||||
'production'
|
||||
)} under the ${chalk.bold(
|
||||
'build'
|
||||
)} key, or specify which profile you'd like to use with the ${chalk.bold(
|
||||
'--profile'
|
||||
)} flag. This fallback behavior will be removed in the next major version of EAS CLI.`
|
||||
);
|
||||
return {
|
||||
profile,
|
||||
profileName: 'release',
|
||||
platform,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!(error instanceof errors.MissingProfileError)) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const defaultProfile = getDefaultProfile({ platform, type });
|
||||
if (defaultProfile) {
|
||||
return {
|
||||
profile: defaultProfile,
|
||||
profileName: '__default__',
|
||||
platform,
|
||||
};
|
||||
}
|
||||
|
||||
const profileNames = await readProfileNamesAsync({
|
||||
projectDir,
|
||||
type,
|
||||
});
|
||||
if (profileNames.length === 0) {
|
||||
throw new errors.MissingProfileError(
|
||||
`Missing profile in eas.json: ${profileName ?? 'production'}`
|
||||
);
|
||||
}
|
||||
const choices: ExpoChoice<string>[] = profileNames.map(profileName => ({
|
||||
title: profileName,
|
||||
value: profileName,
|
||||
}));
|
||||
const chosenProfileName = await selectAsync(
|
||||
'The "production" profile is missing in eas.json. Pick another profile:',
|
||||
choices
|
||||
);
|
||||
const profile = await readProfileAsync({
|
||||
projectDir,
|
||||
easJsonReader,
|
||||
platform,
|
||||
type,
|
||||
profileName: chosenProfileName,
|
||||
profileName,
|
||||
});
|
||||
return {
|
||||
profile,
|
||||
profileName: chosenProfileName,
|
||||
profileName: profileName ?? 'production',
|
||||
platform,
|
||||
};
|
||||
});
|
||||
@@ -134,49 +40,19 @@ export async function getProfilesAsync<T extends ProfileType>({
|
||||
}
|
||||
|
||||
async function readProfileAsync<T extends ProfileType>({
|
||||
projectDir,
|
||||
easJsonReader,
|
||||
platform,
|
||||
type,
|
||||
profileName,
|
||||
}: {
|
||||
projectDir: string;
|
||||
easJsonReader: EasJsonReader;
|
||||
platform: Platform;
|
||||
type: T;
|
||||
profileName: string;
|
||||
profileName?: string;
|
||||
}): Promise<EasProfile<T>> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
if (type === 'build') {
|
||||
return (await easJsonReader.getBuildProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
} else {
|
||||
return (await easJsonReader.getSubmitProfileAsync(platform, profileName)) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultProfile<T extends ProfileType>({
|
||||
platform,
|
||||
type,
|
||||
}: {
|
||||
platform: Platform;
|
||||
type: T;
|
||||
}): EasProfile<T> | null {
|
||||
if (type === 'build') {
|
||||
return null;
|
||||
} else {
|
||||
return getDefaultSubmitProfile(platform) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
async function readProfileNamesAsync({
|
||||
projectDir,
|
||||
type,
|
||||
}: {
|
||||
projectDir: string;
|
||||
type: ProfileType;
|
||||
}): Promise<string[]> {
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
if (type === 'build') {
|
||||
return await easJsonReader.getBuildProfileNamesAsync();
|
||||
} else {
|
||||
return await easJsonReader.getSubmitProfileNamesAsync();
|
||||
}
|
||||
}
|
||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user