mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd533b5fec | ||
|
|
5e857318b8 | ||
|
|
0091d7135b | ||
|
|
7c41d5c39a |
No files matched your search
@@ -2,6 +2,4 @@
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
packages/eas-cli/src/graphql/generated.ts
|
||||
|
||||
node_modules
|
||||
+13
-36
@@ -1,9 +1,15 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['universe/node'],
|
||||
plugins: ['async-protect', 'node'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/*'],
|
||||
},
|
||||
],
|
||||
globals: {
|
||||
jasmine: false,
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-constant-condition': ['warn', { checkLoops: false }],
|
||||
'sort-imports': [
|
||||
'warn',
|
||||
@@ -11,40 +17,11 @@ module.exports = {
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{ devDependencies: ['**/__tests__/**/*', '**/__mocks__/**/*'] },
|
||||
],
|
||||
'import/no-relative-packages': 'error',
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: 'lodash',
|
||||
message: "Don't use lodash, it's heavy!",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'async-protect/async-suffix': 'error',
|
||||
'node/no-sync': 'error',
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/**/*.ts', '**/__mocks__/**/*.ts'],
|
||||
rules: {
|
||||
'async-protect/async-suffix': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -1,58 +0,0 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'needs review'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "We're looking to keep questions on our forums and bug reports on the GitHub repo. For questions and help using EAS CLI, post in our [community forums](https://forums.expo.dev/c/help-expo-application-services) (log in with your Expo developer account)."
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Found a bug in the Expo SDK? Open a bug report [here](https://github.com/expo/expo/issues/new?assignees=&labels=needs+review&template=bug_report.yml) instead.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: If you leave out sections there is a high likelihood your issue will be closed. If you have a question, not a bug report, please post it on our [forums](https://forums.expo.dev/) instead.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Build/Submit details page URL
|
||||
description: If you're reporting an issue with EAS Build and/or EAS Submit, please provide the URL to the details page URL. The page can be only viewed by the Expo team members. Providing the URL is optional but it makes debugging the issue easier.
|
||||
placeholder: https://expo.dev/accounts/ACCOUNT_NAME/projects/PROJECT_NAME/builds/BUILD_ID
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Describe the issue in 1 or 2 sentences
|
||||
placeholder: Clearly describe what the expected behavior is vs. what is actually happening. This should be as short as possible, while still communicating all the necessary information. If your summary is just 'X library/method isn't working', then you need to continue debugging yourself and provide more information.
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Managed or bare?
|
||||
description: Is your project managed or bare? Bare - Android and/or iOS native projects exist in your repo; Managed - you don't have native code (custom or 3rd party) in your project. See the [Workflows](https://docs.expo.dev/introduction/managed-vs-bare/) page for more information.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: Error output
|
||||
placeholder: If you're running into an error message, please run the same command with `EXPO_DEBUG=true <your command here>` and paste output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Reproducible demo or steps to reproduce from a blank project
|
||||
description: 'This should include as little code as possible, do not simply link your entire project. Sharing a link to a [Snack](https://snack.expo.dev/) is a GREAT way to provide a reproducible demo :) If a reproducible demo, or a complete list of steps from blank project to bug, are not provided, it is very likely your issue will be closed. Read [here more guidance](https://stackoverflow.com/help/mcve).'
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Please make sure contributors can follow the steps your provided in order to reproduce the bug.
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: "**Realize that it is up to you to debug your code and be as certain as possible that the bug is with EAS CLI, not with your own app.** [Here's an excellent guide to debugging you can follow](https://github.com/expo/fyi/blob/master/manual-debugging.md#how-to-narrow-down-the-source-of-an-error-a-step-by-step-guide-to-good-old-fashioned-manual-debugging)."
|
||||
@@ -1,14 +0,0 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement,needs review'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thanks for helping us make EAS even better! 😀 Unfortunately, this is not the right place for feature requests. Please find and upvote your feature (or create a new one) at https://expo.canny.io instead.
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Do you understand that any feature requests opened in this repository will be closed?
|
||||
options:
|
||||
- 'Yes'
|
||||
@@ -1,17 +0,0 @@
|
||||
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
|
||||
|
||||
# Checklist
|
||||
|
||||
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
|
||||
|
||||
# Why
|
||||
|
||||
Please describe the motivation for this PR, and link to relevant GitHub issues, forums posts, or feature requests.
|
||||
|
||||
# How
|
||||
|
||||
How did you build this feature or fix this bug and why?
|
||||
|
||||
# Test Plan
|
||||
|
||||
Please describe how you tested this change and how a reviewer could reproduce your test, especially if this PR does not include automated tests! If possible, please also provide terminal output and/or screenshots demonstrating your test/reproduction.
|
||||
@@ -1,46 +0,0 @@
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
name: Changelog
|
||||
|
||||
jobs:
|
||||
pr-commented:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Checkout Pull Request
|
||||
run: hub pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Add changelog entry
|
||||
working-directory: ./scripts
|
||||
run: |
|
||||
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
||||
if [[ ! "$PARAMS" =~ " " ]]; then
|
||||
echo "Invalid /changelog-entry command"
|
||||
exit 1
|
||||
fi
|
||||
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
||||
env:
|
||||
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
||||
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
||||
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
# use the last commit author name and email
|
||||
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
@@ -0,0 +1,18 @@
|
||||
name: Installer tests
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run the installer
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: bash bin/install.sh
|
||||
- run: eas --version
|
||||
@@ -1,146 +0,0 @@
|
||||
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'
|
||||
})
|
||||
+23
-109
@@ -15,7 +15,7 @@ jobs:
|
||||
- id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
draft: true
|
||||
@@ -25,30 +25,21 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '14'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- id: arm
|
||||
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
run: yarn oclif-dev pack --targets linux-x64,linux-arm
|
||||
- name: Upload linux-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/eas-linux-x64.tar.gz
|
||||
asset_name: eas-linux-x64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload linux-arm tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -56,67 +47,57 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.arm.outputs.filename }}
|
||||
asset_name: ${{ steps.arm.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/eas-linux-arm.tar.gz
|
||||
asset_name: eas-linux-arm.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
build-mac:
|
||||
name: Build for macOS
|
||||
# Use macOS to get the correct platform specific dependencies
|
||||
# Use macOS to get the platform specific dependencies like traveling-fastlane-darwin
|
||||
runs-on: macos-latest
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '14'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets darwin-x64
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
run: yarn oclif-dev pack --targets darwin-x64
|
||||
- name: Upload darwin-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/eas-darwin-x64.tar.gz
|
||||
asset_name: eas-darwin-x64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: windows-2019
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '14'
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
- run: |
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:win
|
||||
- run: yarn oclif-dev pack:win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
|
||||
- id: x86
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
|
||||
- name: Upload x64 installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x64.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/win/${{ steps.x64.outputs.filename }}
|
||||
asset_name: ${{ steps.x64.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
- name: Upload x86 installer
|
||||
@@ -125,73 +106,6 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: packages/eas-cli/dist/win32/${{ steps.x86.outputs.filename }}
|
||||
asset_path: packages/eas-cli/dist/win/${{ steps.x86.outputs.filename }}
|
||||
asset_name: ${{ steps.x86.outputs.filename }}
|
||||
asset_content_type: application/gzip
|
||||
publish-to-npm:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Resolve version
|
||||
run: echo "EAS_CLI_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||
- name: Update changelog
|
||||
working-directory: ./scripts
|
||||
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||
- name: Publish release
|
||||
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Commit and push
|
||||
run: |
|
||||
git fetch
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
git add CHANGELOG.md
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
tail -n +3 /tmp/current-changelog > /tmp/changelog.md
|
||||
npx slackmd /tmp/changelog.md
|
||||
echo 'SLACK_CHANGELOG<<EOF' >> $GITHUB_ENV
|
||||
cat /tmp/changelog.slack.md >> $GITHUB_ENV
|
||||
echo 'EOF' >> $GITHUB_ENV
|
||||
- name: Slack Notification
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: eas-cli
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_ICON: https://github.com/expo-ci.png?size=48
|
||||
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
||||
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
||||
SLACK_USERNAME: 'Expo CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
asset_content_type: application/gzip
|
||||
@@ -1,20 +0,0 @@
|
||||
name: Size
|
||||
on:
|
||||
pull_request:
|
||||
types: [synchronize, opened]
|
||||
jobs:
|
||||
compare:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
- uses: preactjs/compressed-size-action@v2
|
||||
with:
|
||||
build-script: 'build:tarballs:linux'
|
||||
repo-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
pattern: './packages/eas-cli/dist/*.tar.gz'
|
||||
compression: 'none'
|
||||
strip-hash: "v\\d+.\\d+.\\d+-\\w+-"
|
||||
clean-script: 'clean'
|
||||
@@ -1,24 +0,0 @@
|
||||
name: Close inactive issues
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v4
|
||||
with:
|
||||
ascending: false
|
||||
operations-per-run: 300
|
||||
days-before-issue-stale: 30
|
||||
days-before-issue-close: 7
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity. If there is no activity in the next 7 days, the issue will be closed."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale. Please open a new issue if you believe you are encountering a related problem."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
enable-statistics: true
|
||||
exempt-issue-labels: "needs review"
|
||||
exempt-all-assignees: true
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
@@ -10,31 +10,24 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['14', '16', '18']
|
||||
node: ['12', '14']
|
||||
include:
|
||||
- node: '18'
|
||||
- node: '14'
|
||||
coverage: true
|
||||
name: Test with Node ${{ matrix.node }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
- run: yarn test
|
||||
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: yarn install --frozen-lockfile --check-files
|
||||
working-directory: ./scripts
|
||||
- run: yarn lint-changelog
|
||||
- run: yarn lint
|
||||
- run: shellcheck bin/*.sh
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
|
||||
+9
-17
@@ -1,21 +1,13 @@
|
||||
# Build directories
|
||||
packages/*/build
|
||||
packages/eas-cli/dist
|
||||
packages/eas-cli/tmp
|
||||
scripts/build
|
||||
|
||||
# Code editors
|
||||
.idea
|
||||
.history
|
||||
.vscode
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
*-debug.log
|
||||
*-error.log
|
||||
|
||||
# Tests
|
||||
**/build/**
|
||||
!packages/eas-cli/src/commands/build/**
|
||||
!packages/eas-cli/src/build/**
|
||||
|
||||
dist
|
||||
/package-lock.json
|
||||
tmp
|
||||
node_modules
|
||||
coverage
|
||||
.history
|
||||
+2
-3
@@ -2,8 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
-1549
File diff suppressed because it is too large.
Load diff
-16
@@ -1,16 +0,0 @@
|
||||
* @dsokal @wkozyra95
|
||||
|
||||
packages/eas-cli/src/build @dsokal @wkozyra95
|
||||
packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||
|
||||
packages/eas-cli/src/submit @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @dsokal
|
||||
|
||||
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
|
||||
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
|
||||
|
||||
packages/eas-cli/schema @bycedric
|
||||
packages/eas-cli/src/metadata @bycedric
|
||||
packages/eas-cli/src/commands/metadata @bycedric
|
||||
|
||||
packages/eas-json @dsokal @wkozyra95
|
||||
@@ -1,7 +0,0 @@
|
||||
# Community Code of Conduct
|
||||
|
||||
The Expo team is committed to fostering an open and welcoming environment.
|
||||
|
||||
**Our Community Guidelines can be found here:**
|
||||
|
||||
https://expo.dev/guidelines
|
||||
+6
-2
@@ -1,4 +1,8 @@
|
||||
## Releasing a new version
|
||||
|
||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
1. Add release notes to `CHANGELOG.md`.
|
||||
1. Run `yarn release` in the repository root folder. This will create a new tag
|
||||
like `v1.0.0`.
|
||||
1. Wait until the `release` GitHub workflow has finished.
|
||||
1. Once the release ready to go, you can go to
|
||||
https://github.com/expo/eas-cli/releases and mark it as not draft.
|
||||
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2020-present 650 Industries, Inc. (aka Expo)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script downloads the latest release version of EAS CLI from GitHub and
|
||||
# installs it in /usr/local/bin
|
||||
|
||||
{
|
||||
# All code is inside a block to ensure the script executes only when
|
||||
# downloaded completely.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
prefix="${EAS_PREFIX:-/usr/local}"
|
||||
temp_dir="$(mktemp -d)"
|
||||
|
||||
if [ -t 1 ] && [ -z "$NO_COLOR" ]
|
||||
then
|
||||
# Use colors when stdout is a terminal and NO_COLOR isn't set.
|
||||
text_bold=$'\033[1m'
|
||||
text_normal=$'\033[22m'
|
||||
text_red=$'\033[31m'
|
||||
text_default_color=$'\033[39m'
|
||||
else
|
||||
# Disable colors
|
||||
text_bold=""
|
||||
text_normal=""
|
||||
text_red=""
|
||||
text_default_color=""
|
||||
fi
|
||||
|
||||
abort() {
|
||||
echo -e "$text_bold$text_red$1$text_normal$text_default_color"
|
||||
exit 1
|
||||
}
|
||||
|
||||
install() {
|
||||
needs_sudo=""
|
||||
# Make sure path exists and can be written to by the current user.
|
||||
if ! mkdir -p "$prefix/lib" || [[ ! -w "$prefix/lib" ]]
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
if ! mkdir -p "$prefix/bin" || [[ ! -w "$prefix/bin" ]]
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
if ! rm -rf "$prefix/lib/eas"
|
||||
then
|
||||
needs_sudo=true
|
||||
fi
|
||||
# Run commands with sudo if necessary.
|
||||
if [ -n "$needs_sudo" ]
|
||||
then
|
||||
if [ "$EUID" -ne 0 ]
|
||||
then
|
||||
echo "Installing requires superuser access."
|
||||
echo "The sudo command will prompt for your password."
|
||||
fi
|
||||
sudo rm -rf "$prefix/lib/eas"
|
||||
sudo mv "$temp_dir/eas" "$prefix/lib/eas"
|
||||
sudo ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
|
||||
else
|
||||
mv "$temp_dir/eas" "$prefix/lib/eas"
|
||||
ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
|
||||
fi
|
||||
}
|
||||
|
||||
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
|
||||
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
|
||||
fi
|
||||
|
||||
hardware="$(uname -m)"
|
||||
case "$hardware" in
|
||||
x86_64)
|
||||
arch="x64"
|
||||
;;
|
||||
arm*)
|
||||
arch="arm"
|
||||
;;
|
||||
*)
|
||||
abort "Unsupported CPU architecture $hardware."
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
platform="darwin"
|
||||
if [[ "$arch" != "x64" ]]; then
|
||||
abort "Unsupported CPU architecture $hardware on macOS."
|
||||
fi
|
||||
;;
|
||||
Linux*)
|
||||
platform="linux"
|
||||
;;
|
||||
*)
|
||||
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
|
||||
;;
|
||||
esac
|
||||
|
||||
url="https://github.com/expo/eas-cli/releases/latest/download/eas-$platform-$arch.tar.gz"
|
||||
echo "Installing EAS CLI from $url"
|
||||
|
||||
mkdir -p "$temp_dir"
|
||||
cd "$temp_dir"
|
||||
|
||||
echo
|
||||
echo "Downloading..."
|
||||
if [ "$(command -v curl)" ]
|
||||
then
|
||||
curl --location --progress-bar "$url" | tar xz
|
||||
else
|
||||
wget -O- "$url" | tar xz
|
||||
fi
|
||||
|
||||
# Move the installation over and link the executable (may require sudo)
|
||||
install
|
||||
|
||||
# Test the bundled node binary and if it doesn't work remove it (fall back to
|
||||
# node installed in path).
|
||||
"$prefix/lib/eas/bin/node" -v > /dev/null || {
|
||||
rm "$prefix/lib/eas/bin/node"
|
||||
}
|
||||
|
||||
# Test the CLI
|
||||
executable=$(command -v eas) || abort "Could not run eas after installation."
|
||||
echo
|
||||
echo -e "EAS CLI is installed to ${executable}"
|
||||
eas --version
|
||||
|
||||
echo
|
||||
echo -e "Type ${text_bold}eas --help${text_normal} to get started."
|
||||
}
|
||||
+5
-4
@@ -1,3 +1,4 @@
|
||||
comment: false
|
||||
coverage:
|
||||
round: up
|
||||
precision: 2
|
||||
@@ -7,11 +8,11 @@ coverage:
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
config:
|
||||
informational: true
|
||||
paths:
|
||||
- packages/config
|
||||
eas-cli:
|
||||
informational: true
|
||||
paths:
|
||||
- packages/eas-cli
|
||||
eas-json:
|
||||
informational: true
|
||||
paths:
|
||||
- packages/eas-json
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
projects: [
|
||||
require('./packages/eas-cli/jest.config.js'),
|
||||
require('./packages/eas-json/jest.config.js'),
|
||||
require('./packages/config/jest.config.js'),
|
||||
],
|
||||
testPathIgnorePatterns: ['.*'],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
module.exports = function (api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
// Only use this when running tests
|
||||
env: {
|
||||
test: {
|
||||
presets: ['@expo/babel-preset-cli'],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||
transform: {
|
||||
'^.+\\.[jt]sx?$': ['babel-jest', { configFile: require.resolve('./babel.config.js') }],
|
||||
},
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
};
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.0.0"
|
||||
"version": "0.1.0-alpha.2"
|
||||
}
|
||||
+11
-28
@@ -4,36 +4,19 @@
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "lerna run build",
|
||||
"build:tarballs:linux": "yarn build-for-size-check",
|
||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --stream --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "./scripts/bin/run release",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
"lint": "eslint . --ext .ts",
|
||||
"release": "lerna version",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.2.0",
|
||||
"@types/node": "18.11.5",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-universe": "11.1.0",
|
||||
"eslint-plugin-async-protect": "2.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "29.2.2",
|
||||
"jest-watch-typeahead": "2.2.0",
|
||||
"lerna": "6.0.1",
|
||||
"oclif": "3.2.22",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "29.0.3",
|
||||
"typescript": "4.8.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
"yarn": "1.22.19"
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "^26.0.13",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-universe": "^4.0.0",
|
||||
"jest": "^26.4.2",
|
||||
"jest-watch-typeahead": "^0.6.1",
|
||||
"lerna": "^3.22.1",
|
||||
"prettier": "^2.0.5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -1,3 +1,3 @@
|
||||
# @expo/eas-json
|
||||
# @eas/config
|
||||
|
||||
A library for interacting with the eas.json
|
||||
@@ -0,0 +1,2 @@
|
||||
import { fs } from 'memfs';
|
||||
module.exports = fs;
|
||||
File renamed without changes.
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@eas/config",
|
||||
"description": "A library for interacting with the eas.json",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/eas-build-job": "0.1.1",
|
||||
"@hapi/joi": "^17.1.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"tslib": "^1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@expo/babel-preset-cli": "^0.2.17",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/hapi__joi": "^17.1.4",
|
||||
"@types/node": "^12",
|
||||
"babel-jest": "^26.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"license": "MIT",
|
||||
"main": "src/index.ts",
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"test": "jest"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Workflow is representing different value than BuildType from @expo/eas-build-job
|
||||
// Each workflow has a set of BuildTypes available
|
||||
// - Generic workflow allows to build 'generic' and 'generic-client'
|
||||
// - Managed workflow allows to build 'managed' and 'managed-client'
|
||||
export enum Workflow {
|
||||
Generic = 'generic',
|
||||
Managed = 'managed',
|
||||
}
|
||||
|
||||
export enum CredentialsSource {
|
||||
LOCAL = 'local',
|
||||
REMOTE = 'remote',
|
||||
AUTO = 'auto',
|
||||
}
|
||||
|
||||
export interface AndroidManagedBuildProfile {
|
||||
workflow: Workflow.Managed;
|
||||
credentialsSource: CredentialsSource;
|
||||
buildType?: 'apk' | 'app-bundle';
|
||||
releaseChannel?: undefined;
|
||||
}
|
||||
|
||||
export interface AndroidGenericBuildProfile {
|
||||
workflow: Workflow.Generic;
|
||||
credentialsSource: CredentialsSource;
|
||||
gradleCommand?: string;
|
||||
releaseChannel?: string;
|
||||
artifactPath?: string;
|
||||
withoutCredentials?: boolean;
|
||||
}
|
||||
|
||||
export interface iOSManagedBuildProfile {
|
||||
workflow: Workflow.Managed;
|
||||
credentialsSource: CredentialsSource;
|
||||
buildType?: 'archive' | 'simulator';
|
||||
releaseChannel?: undefined;
|
||||
}
|
||||
|
||||
export interface iOSGenericBuildProfile {
|
||||
workflow: Workflow.Generic;
|
||||
credentialsSource: CredentialsSource;
|
||||
scheme?: string;
|
||||
releaseChannel?: string;
|
||||
artifactPath?: string;
|
||||
}
|
||||
|
||||
export type AndroidBuildProfile = AndroidManagedBuildProfile | AndroidGenericBuildProfile;
|
||||
export type iOSBuildProfile = iOSManagedBuildProfile | iOSGenericBuildProfile;
|
||||
export type BuildProfile = AndroidBuildProfile | iOSBuildProfile;
|
||||
|
||||
// EasConfig represents eas.json with one specific profile
|
||||
export interface EasConfig {
|
||||
builds: {
|
||||
android?: AndroidManagedBuildProfile | AndroidGenericBuildProfile;
|
||||
ios?: iOSManagedBuildProfile | iOSGenericBuildProfile;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import {
|
||||
AndroidBuildProfile,
|
||||
BuildProfile,
|
||||
EasConfig,
|
||||
Workflow,
|
||||
iOSBuildProfile,
|
||||
} from './Config.types';
|
||||
import { EasJsonSchema, schemaBuildProfileMap } from './EasJsonSchema';
|
||||
|
||||
interface EasJson {
|
||||
builds: {
|
||||
android?: { [key: string]: BuildProfilePreValidation };
|
||||
ios?: { [key: string]: BuildProfilePreValidation };
|
||||
};
|
||||
}
|
||||
|
||||
interface BuildProfilePreValidation {
|
||||
workflow: Workflow;
|
||||
}
|
||||
|
||||
export class EasJsonReader {
|
||||
constructor(private projectDir: string, private platform: 'android' | 'ios' | 'all') {}
|
||||
|
||||
public async readAsync(buildProfileName: string): Promise<EasConfig> {
|
||||
const easJson = await this.readRawAsync();
|
||||
|
||||
let androidConfig;
|
||||
if (['android', 'all'].includes(this.platform)) {
|
||||
androidConfig = this.validateBuildProfile<AndroidBuildProfile>(
|
||||
Platform.Android,
|
||||
buildProfileName,
|
||||
easJson.builds?.android?.[buildProfileName]
|
||||
);
|
||||
}
|
||||
let iosConfig;
|
||||
if (['ios', 'all'].includes(this.platform)) {
|
||||
iosConfig = this.validateBuildProfile<iOSBuildProfile>(
|
||||
Platform.iOS,
|
||||
buildProfileName,
|
||||
easJson.builds?.ios?.[buildProfileName]
|
||||
);
|
||||
}
|
||||
return {
|
||||
builds: {
|
||||
...(androidConfig ? { android: androidConfig } : {}),
|
||||
...(iosConfig ? { ios: iosConfig } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async readRawAsync(): Promise<EasJson> {
|
||||
const rawFile = await fs.readFile(path.join(this.projectDir, 'eas.json'), 'utf-8');
|
||||
const json = JSON.parse(rawFile);
|
||||
|
||||
const { value, error } = EasJsonSchema.validate(json, {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(`eas.json is not valid [${error.toString()}]`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private validateBuildProfile<T extends BuildProfile>(
|
||||
platform: 'android' | 'ios' | 'all',
|
||||
buildProfileName: string,
|
||||
buildProfile?: BuildProfilePreValidation
|
||||
): T {
|
||||
if (!buildProfile) {
|
||||
throw new Error(`There is no profile named ${buildProfileName} for platform ${platform}`);
|
||||
}
|
||||
const schema = schemaBuildProfileMap[platform][buildProfile?.workflow];
|
||||
if (!schema) {
|
||||
throw new Error('invalid workflow'); // this should be validated earlier
|
||||
}
|
||||
const { value, error } = schema.validate(buildProfile, {
|
||||
stripUnknown: true,
|
||||
convert: true,
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw new Error(
|
||||
`Object "${platform}.${buildProfileName}" in eas.json is not valid [${error.toString()}]`
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import Joi from '@hapi/joi';
|
||||
|
||||
const AndroidGenericSchema = Joi.object({
|
||||
workflow: Joi.string().valid('generic').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
gradleCommand: Joi.string(),
|
||||
releaseChannel: Joi.string(),
|
||||
artifactPath: Joi.string(),
|
||||
withoutCredentials: Joi.boolean(),
|
||||
});
|
||||
|
||||
const AndroidManagedSchema = Joi.object({
|
||||
workflow: Joi.string().valid('managed').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
buildType: Joi.string().valid('apk', 'app-bundle').default('app-bundle'),
|
||||
});
|
||||
|
||||
const iOSGenericSchema = Joi.object({
|
||||
workflow: Joi.string().valid('generic').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
scheme: Joi.string(),
|
||||
releaseChannel: Joi.string(),
|
||||
artifactPath: Joi.string(),
|
||||
});
|
||||
|
||||
const iOSManagedSchema = Joi.object({
|
||||
workflow: Joi.string().valid('managed').required(),
|
||||
credentialsSource: Joi.string().valid('local', 'remote', 'auto').default('auto'),
|
||||
buildType: Joi.string().valid('archive', 'simulator'),
|
||||
});
|
||||
|
||||
const schemaBuildProfileMap: Record<string, Record<string, Joi.Schema>> = {
|
||||
android: {
|
||||
generic: AndroidGenericSchema,
|
||||
managed: AndroidManagedSchema,
|
||||
},
|
||||
ios: {
|
||||
managed: iOSManagedSchema,
|
||||
generic: iOSGenericSchema,
|
||||
},
|
||||
};
|
||||
|
||||
const EasJsonSchema = Joi.object({
|
||||
builds: Joi.object({
|
||||
android: Joi.object().pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
workflow: Joi.string().valid('generic', 'managed').required(),
|
||||
}).unknown(true) // profile is validated further only if build is for that platform
|
||||
),
|
||||
ios: Joi.object().pattern(
|
||||
Joi.string(),
|
||||
Joi.object({
|
||||
workflow: Joi.string().valid('generic', 'managed').required(),
|
||||
}).unknown(true) // profile is validated further only if build is for that platform
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
export { EasJsonSchema, schemaBuildProfileMap };
|
||||
@@ -0,0 +1,191 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
|
||||
import { EasJsonReader } from '../EasJsonReader';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
await fs.mkdirp('/project');
|
||||
});
|
||||
|
||||
test('minimal valid android eas.json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
android: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const easJson = await reader.readAsync('release');
|
||||
expect({
|
||||
builds: {
|
||||
android: {
|
||||
workflow: 'generic',
|
||||
credentialsSource: 'auto',
|
||||
},
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
});
|
||||
|
||||
test('minimal valid ios eas.json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
ios: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'ios');
|
||||
const easJson = await reader.readAsync('release');
|
||||
expect({
|
||||
builds: {
|
||||
ios: {
|
||||
credentialsSource: 'auto',
|
||||
workflow: 'generic',
|
||||
},
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
});
|
||||
|
||||
test('minimal valid eas.json for both platforms', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
android: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
ios: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'all');
|
||||
const easJson = await reader.readAsync('release');
|
||||
expect({
|
||||
builds: {
|
||||
android: { workflow: 'generic', credentialsSource: 'auto' },
|
||||
ios: { workflow: 'generic', credentialsSource: 'auto' },
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
});
|
||||
|
||||
test('valid eas.json with both platform, but reading only android', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
ios: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
android: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const easJson = await reader.readAsync('release');
|
||||
expect({
|
||||
builds: {
|
||||
android: { workflow: 'generic', credentialsSource: 'auto' },
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
});
|
||||
|
||||
test('valid eas.json for debug builds', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
ios: {
|
||||
release: { workflow: 'managed' },
|
||||
debug: { workflow: 'managed', buildType: 'simulator' },
|
||||
},
|
||||
android: {
|
||||
release: { workflow: 'generic' },
|
||||
debug: {
|
||||
workflow: 'generic',
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
withoutCredentials: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'all');
|
||||
const easJson = await reader.readAsync('debug');
|
||||
expect({
|
||||
builds: {
|
||||
android: {
|
||||
credentialsSource: 'auto',
|
||||
workflow: 'generic',
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
withoutCredentials: true,
|
||||
},
|
||||
ios: {
|
||||
credentialsSource: 'auto',
|
||||
workflow: 'managed',
|
||||
buildType: 'simulator',
|
||||
},
|
||||
},
|
||||
}).toEqual(easJson);
|
||||
});
|
||||
|
||||
test('invalid eas.json with missing preset', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
android: {
|
||||
release: { workflow: 'generic' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const promise = reader.readAsync('debug');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'There is no profile named debug for platform android'
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid eas.json when using buildType for wrong platform', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
android: {
|
||||
release: { workflow: 'managed', buildType: 'archive' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const promise = reader.readAsync('release');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'Object "android.release" in eas.json is not valid [ValidationError: "buildType" must be one of [apk, app-bundle]]'
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid eas.json when missing workflow', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
builds: {
|
||||
android: {
|
||||
release: { buildType: 'apk' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const promise = reader.readAsync('release');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'eas.json is not valid [ValidationError: "builds.android.release.workflow" is required]'
|
||||
);
|
||||
});
|
||||
|
||||
test('empty json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {});
|
||||
|
||||
const reader = new EasJsonReader('/project', 'android');
|
||||
const promise = reader.readAsync('release');
|
||||
await expect(promise).rejects.toThrowError(
|
||||
'There is no profile named release for platform android'
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
export {
|
||||
Workflow,
|
||||
CredentialsSource,
|
||||
AndroidManagedBuildProfile,
|
||||
AndroidGenericBuildProfile,
|
||||
AndroidBuildProfile,
|
||||
iOSManagedBuildProfile,
|
||||
iOSGenericBuildProfile,
|
||||
iOSBuildProfile,
|
||||
EasConfig,
|
||||
} from './Config.types';
|
||||
export { EasJsonReader } from './EasJsonReader';
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"outDir": "build",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"target": "es2017"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
const rootEslintrc = require('../../.eslintrc.js');
|
||||
|
||||
module.exports = {
|
||||
rules: {
|
||||
'no-restricted-properties': [
|
||||
'warn',
|
||||
{
|
||||
object: 'it',
|
||||
property: 'only',
|
||||
message: 'it.only should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'test',
|
||||
property: 'only',
|
||||
message: 'test.only should not be committed to main.',
|
||||
},
|
||||
{
|
||||
object: 'describe',
|
||||
property: 'only',
|
||||
message: 'describe.only should not be committed to main.',
|
||||
},
|
||||
],
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
...rootEslintrc.rules['no-restricted-imports'][1].paths,
|
||||
{
|
||||
name: 'ora',
|
||||
message: 'Import ora from src/ora.ts instead.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
'graphql/template-strings': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/named-operations': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
'graphql/required-fields': [
|
||||
'error',
|
||||
{
|
||||
env: 'apollo',
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
requiredFields: ['id'],
|
||||
},
|
||||
],
|
||||
'graphql/capitalized-type-name': [
|
||||
'error',
|
||||
{
|
||||
schemaJson: require('./graphql.schema.json'),
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: ['graphql'],
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
graphql.schema.json linguist-generated
|
||||
src/graphql/generated.ts linguist-generated
|
||||
+112
-1189
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
||||
const getConfig = jest.fn();
|
||||
|
||||
export { getConfig };
|
||||
@@ -1,14 +1,2 @@
|
||||
import { fs } from 'memfs';
|
||||
|
||||
// needed because of a weird bug with tempy (dependency of @expo/config-plugins)
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync('/tmp');
|
||||
if (process.env.TMPDIR) {
|
||||
// eslint-disable-next-line node/no-sync
|
||||
fs.mkdirSync(process.env.TMPDIR, { recursive: true });
|
||||
}
|
||||
|
||||
// fs-extra@10 is not compatible with memfs, the following line fixes tests
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
@@ -0,0 +1,6 @@
|
||||
export default jest.fn().mockReturnValue({
|
||||
start: jest.fn().mockReturnValue({
|
||||
fail: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
}),
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const oclif = require('@oclif/core');
|
||||
const path = require('path');
|
||||
|
||||
const project = path.join(__dirname, '..', 'tsconfig.json');
|
||||
|
||||
// In dev mode -> use ts-node and dev plugins
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
require('ts-node').register({ project });
|
||||
|
||||
// In dev mode, always show stack traces
|
||||
oclif.settings.disableJsonFlag = true
|
||||
|
||||
// Start the CLI
|
||||
oclif.run().then(oclif.flush).catch(oclif.Errors.handle);
|
||||
@@ -1,3 +0,0 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
require('@oclif/core')
|
||||
require('@oclif/command')
|
||||
.run()
|
||||
.then(require('@oclif/core/flush'))
|
||||
.catch(require('@oclif/core/handle'));
|
||||
.then(require('@oclif/command/flush'))
|
||||
.catch(require('@oclif/errors/handle'));
|
||||
@@ -1,22 +0,0 @@
|
||||
overwrite: true
|
||||
schema: 'https://staging-api.expo.dev/graphql'
|
||||
documents:
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
- 'src/branch/**/*.ts'
|
||||
- 'src/channel/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
- 'typescript'
|
||||
- 'typescript-operations'
|
||||
config:
|
||||
dedupeOperationSuffix: true
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- 'node ./scripts/annotate-graphql-codegen.js'
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- 'introspection'
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,6 +2,4 @@ module.exports = {
|
||||
...require('../../jest/jest.config.js'),
|
||||
rootDir: __dirname,
|
||||
roots: ['src', '__mocks__'],
|
||||
globalSetup: './jest/global-setup.ts',
|
||||
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = async () => {
|
||||
process.env.TZ = 'UTC';
|
||||
process.env.EAS_NO_VCS = '1';
|
||||
process.env.EAS_PROJECT_ROOT = '/app';
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
/* eslint-disable no-console */
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
console.error = jest.fn();
|
||||
});
|
||||
+78
-165
@@ -1,129 +1,88 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.0.0",
|
||||
"author": "Expo <support@expo.dev>",
|
||||
"version": "0.1.0-alpha.2",
|
||||
"author": "Expo <support@expo.io>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||
"@expo/code-signing-certificates": "0.0.3",
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"@expo/eas-json": "3.0.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.18",
|
||||
"@expo/plugin-help": "5.1.16",
|
||||
"@expo/plugin-warn-if-update-available": "2.4.0",
|
||||
"@expo/prebuild-config": "5.0.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.19.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.4",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "3.0.5",
|
||||
"@urql/exchange-retry": "1.0.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.11.2",
|
||||
"dateformat": "4.6.3",
|
||||
"dotenv": "16.0.3",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.12",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"getenv": "1.0.0",
|
||||
"gradle-to-js": "2.0.1",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.2.0",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.6.4",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.1.0",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pngjs": "6.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
"promise-retry": "2.0.1",
|
||||
"prompts": "2.4.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.8",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
"@eas/config": "^0.1.0-alpha.2",
|
||||
"@expo/config": "^3.3.12",
|
||||
"@expo/eas-build-job": "0.1.1",
|
||||
"@expo/json-file": "^8.2.24",
|
||||
"@expo/plist": "^0.0.10",
|
||||
"@expo/results": "^1.0.0",
|
||||
"@expo/spawn-async": "^1.5.0",
|
||||
"@oclif/command": "^1",
|
||||
"@oclif/config": "^1",
|
||||
"@oclif/plugin-help": "^3",
|
||||
"@urql/core": "^1.13.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-table3": "^0.6.0",
|
||||
"dateformat": "^3.0.3",
|
||||
"env-paths": "^2.2.0",
|
||||
"form-data": "^3.0.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"got": "^11.5.2",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"keychain": "^1.3.0",
|
||||
"lodash": "^4.17.20",
|
||||
"md5-file": "^5.0.0",
|
||||
"minimatch": "^3.0.4",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"ora": "^5.1.0",
|
||||
"pkg-dir": "^4.2.0",
|
||||
"progress": "^2.0.3",
|
||||
"prompts": "^2.3.2",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"tar": "^6.0.5",
|
||||
"tslib": "^1",
|
||||
"untildify": "^4.0.0",
|
||||
"uuid": "^8.3.0",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.13.7",
|
||||
"@graphql-codegen/introspection": "2.2.1",
|
||||
"@graphql-codegen/typescript": "2.8.0",
|
||||
"@graphql-codegen/typescript-operations": "2.5.5",
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.3.0",
|
||||
"@types/pngjs": "6.0.1",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.4.1",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.18.2",
|
||||
"memfs": "3.4.7",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "4.0.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.8.4"
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@expo/babel-preset-cli": "^0.2.17",
|
||||
"@types/color-string": "^1.5.0",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/lodash": "^4.14.161",
|
||||
"@types/node": "^12",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
"@types/progress": "^2.0.3",
|
||||
"@types/prompts": "^2.0.8",
|
||||
"@types/tar": "^4.0.3",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"@types/wordwrap": "^1.0.0",
|
||||
"@vercel/ncc": "^0.23.0",
|
||||
"babel-jest": "^26.3.0",
|
||||
"graphql": "^15.3.0",
|
||||
"memfs": "^3.2.0",
|
||||
"tempy": "^0.7.0",
|
||||
"ts-node": "^8",
|
||||
"typescript": "^3.9.7",
|
||||
"wonka": "^4.0.14"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@expo/traveling-fastlane-darwin": "1.15.1",
|
||||
"@expo/traveling-fastlane-linux": "1.15.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/build",
|
||||
"/oclif.manifest.json",
|
||||
"/schema"
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"keywords": [
|
||||
@@ -132,57 +91,25 @@
|
||||
"cli"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"oclif": {
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@expo/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
"@oclif/plugin-help"
|
||||
],
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage account"
|
||||
},
|
||||
"branch": {
|
||||
"description": "manage update branches"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
},
|
||||
"channel": {
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage Apple devices for Internal Distribution"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "manage store configuration"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
},
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
"description": "roll out changes"
|
||||
},
|
||||
"webhook": {
|
||||
"description": "manage webhooks"
|
||||
}
|
||||
},
|
||||
"warn-if-update-available": {
|
||||
"timeoutInDays": 0.5,
|
||||
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
|
||||
},
|
||||
"update": {
|
||||
"node": {
|
||||
"version": "18.6.0"
|
||||
},
|
||||
"s3": {
|
||||
"templates": {
|
||||
"target": {
|
||||
@@ -191,29 +118,15 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"-h"
|
||||
],
|
||||
"additionalVersionFlags": [
|
||||
"-v"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rimraf oclif.manifest.json",
|
||||
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && oclif-dev manifest && oclif-dev readme",
|
||||
"build": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"clean": "rimraf dist build tmp node_modules yarn-error.log"
|
||||
"version": "oclif-dev readme && git add README.md"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
"yarn": "1.22.19"
|
||||
}
|
||||
"types": "build/index.d.ts"
|
||||
}
|
||||
@@ -1,793 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"apple": {
|
||||
"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",
|
||||
"description": "Configure how the app is released in the app store.",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"automaticRelease": {
|
||||
"description": "If and how the app should automatically be released when approved by App Store review.",
|
||||
"markdownDescription": "If and how the app should automatically be released when approved by App Store review.\n- `false` - Manually release the app after store approval. (default behavior)\n- `true` - Automatically release after store approval.\n- `Date` - Automatically schedule release on this date, after store approval.\n\nApple does not guarantee that your app is available at the chosen scheduled release date.",
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
{
|
||||
"type": "boolean"
|
||||
}
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"label": "Manual release",
|
||||
"description": "Manually release the app after store approval",
|
||||
"body": false
|
||||
},
|
||||
{
|
||||
"label": "Automatic release",
|
||||
"description": "Automatically release the app after store approval",
|
||||
"body": true
|
||||
},
|
||||
{
|
||||
"label": "Scheduled release",
|
||||
"description": "Automatically release the app on this date, and store approval",
|
||||
"body": "$CURRENT_YEAR-${1:$CURRENT_MONTH}-${2:$CURRENT_DATE}T${3:$CURRENT_HOUR}:00:00+00:00"
|
||||
}
|
||||
]
|
||||
},
|
||||
"phasedRelease": {
|
||||
"type": "boolean",
|
||||
"description": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates. Keep in mind that this version will still be available to all users as a manual update from the App Store. You can pause the phased release for up to 30 days or release this update to all users at any time.\n@see https://help.apple.com/app-store-connect/#/dev3d65fcee1",
|
||||
"markdownDescription": "Phased release for automatic updates lets you gradually release this update over a 7-day period to users who have turned on automatic updates.\n\nKeep in mind that this version will still be available to all users as a manual update from the App Store.\n\nYou can pause the phased release for up to 30 days or release this update to all users at any time.\n\n[Learn More](https://help.apple.com/app-store-connect/#/dev3d65fcee1)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "The version string to edit, or create, if it doesn't exist. All the metadata will be stored on this app version.",
|
||||
"minLength": 1,
|
||||
"defaultSnippets": [
|
||||
{
|
||||
"body": "${1:1}.${2:0}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"copyright": {
|
||||
"type": "string",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"advisory": {
|
||||
"$ref": "#/definitions/apple/AppleAdvisory"
|
||||
},
|
||||
"categories": {
|
||||
"$ref": "#/definitions/apple/AppleCategory"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"release": {
|
||||
"$ref": "#/definitions/apple/AppleRelease",
|
||||
"description": "Release strategy"
|
||||
},
|
||||
"review": {
|
||||
"$ref": "#/definitions/apple/AppleReview",
|
||||
"description": "Info for the App Store reviewer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
const fs = require('fs/promises');
|
||||
|
||||
const path = './src/graphql/generated.ts';
|
||||
|
||||
(async () => {
|
||||
const generatedCode = await fs.readFile(path, 'utf8');
|
||||
const comment = `/**
|
||||
* This file was generated using GraphQL Codegen
|
||||
* Command: yarn generate-graphql-code
|
||||
* Run this during development for automatic type generation when editing GraphQL documents
|
||||
* For more info and docs, visit https://graphql-code-generator.com/
|
||||
*/\n\n`;
|
||||
|
||||
await fs.writeFile(path, comment + generatedCode);
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
const fs = require('fs/promises');
|
||||
|
||||
(async () => {
|
||||
// Patch `oclif readme` path and link generation
|
||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||
await fs.writeFile('README.md', readmeContent);
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Patched README path generation');
|
||||
})();
|
||||
@@ -1,7 +0,0 @@
|
||||
const spawn = require('@expo/spawn-async');
|
||||
|
||||
(async () => {
|
||||
if (!process.env.CLI_SIZE_CHECK) {
|
||||
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
|
||||
}
|
||||
})();
|
||||
@@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
EAS_CLI_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. && pwd )"
|
||||
PACKAGE_JSON_PATH="$EAS_CLI_DIR/package.json"
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
else
|
||||
sed -i "s/\\\"@expo\/eas-json\\\": \\\".*\\\"/\\\"@expo\/eas-json\\\": \\\"file:..\/..\/..\/eas-json\\\"/g" $PACKAGE_JSON_PATH
|
||||
fi
|
||||
@@ -1,23 +0,0 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
|
||||
export class ApiV2Error extends Error {
|
||||
override readonly name = 'ApiV2Error';
|
||||
readonly expoApiV2ErrorCode: string;
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
|
||||
constructor(response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}) {
|
||||
super(response.message);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export const ora = jest.fn().mockReturnValue({
|
||||
start: jest.fn().mockReturnValue({
|
||||
fail: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
}),
|
||||
stop: jest.fn(),
|
||||
stopAndPersist: jest.fn(),
|
||||
succeed: jest.fn(),
|
||||
});
|
||||
@@ -1,56 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import nock from 'nock';
|
||||
|
||||
import { ApiV2Error } from '../ApiV2Error';
|
||||
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
|
||||
|
||||
describe(ApiV2Client, () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl())
|
||||
.post('/v2/test')
|
||||
.reply(400, {
|
||||
errors: [
|
||||
{
|
||||
message: 'hellomessage',
|
||||
code: 'TEST_CODE',
|
||||
stack: 'line 1: hello',
|
||||
details: { who: 'world' },
|
||||
metadata: { an: 'object' },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
||||
body: {},
|
||||
});
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(ApiV2Error);
|
||||
assert(error instanceof ApiV2Error);
|
||||
|
||||
expect(error.message).toEqual('hellomessage');
|
||||
expect(error.expoApiV2ErrorCode).toEqual('TEST_CODE');
|
||||
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
|
||||
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
|
||||
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
||||
body: {},
|
||||
});
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).not.toBeInstanceOf(ApiV2Error);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { User } from '../user/User';
|
||||
|
||||
interface MockProject {
|
||||
projectRoot: string;
|
||||
projectTree: Record<string, string>;
|
||||
appJSON: AppJSON;
|
||||
packageJSON: PackageJSON;
|
||||
}
|
||||
|
||||
interface PackageJSON {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
main: string;
|
||||
}
|
||||
|
||||
interface AppJSON {
|
||||
expo: {
|
||||
name: string;
|
||||
version: string;
|
||||
slug: string;
|
||||
sdkVersion: string;
|
||||
owner: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
function createTestProject(
|
||||
user: User,
|
||||
appJSONExtraData?: Record<string, object | string>
|
||||
): MockProject {
|
||||
const projectRoot = '/test-project';
|
||||
const packageJSON: PackageJSON = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSON = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: user.username,
|
||||
...appJSONExtraData,
|
||||
},
|
||||
};
|
||||
return {
|
||||
appJSON,
|
||||
packageJSON,
|
||||
projectRoot,
|
||||
projectTree: {
|
||||
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
|
||||
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { createTestProject };
|
||||
@@ -0,0 +1,3 @@
|
||||
export function asMock(fn: any): jest.Mock {
|
||||
return fn as jest.Mock;
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Actor, getActorDisplayName } from '../user/User';
|
||||
import UserSettings from '../user/UserSettings';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
|
||||
|
||||
export enum CommandEvent {
|
||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||
}
|
||||
|
||||
export enum SubmissionEvent {
|
||||
SUBMIT_COMMAND = 'submit cli submit command',
|
||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||
}
|
||||
|
||||
export enum BuildEvent {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||
|
||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||
|
||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||
|
||||
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 type AnalyticsEventProperties = Record<string, string | number | boolean>;
|
||||
|
||||
/**
|
||||
* The interface for commands to use to log events to analytics.
|
||||
*/
|
||||
export interface Analytics {
|
||||
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
|
||||
* this within EASCommand.
|
||||
*/
|
||||
export interface AnalyticsWithOrchestration extends Analytics {
|
||||
setActor(actor: Actor): void;
|
||||
flushAsync(): Promise<void>;
|
||||
}
|
||||
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
|
||||
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
|
||||
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
|
||||
|
||||
/**
|
||||
* Sets the user's analytics enabled preference. Note that this will only take effect
|
||||
* upon the next run of the CLI.
|
||||
*/
|
||||
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the user's analytics enabled preference.
|
||||
*/
|
||||
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
|
||||
return !!analyticsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an instance of Analytics based on the user's analytics enabled preferences.
|
||||
*/
|
||||
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
|
||||
}
|
||||
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
|
||||
if (!analyticsEnabled) {
|
||||
return new NoOpAnalytics();
|
||||
}
|
||||
|
||||
const persistedDeviceId = await UserSettings.getAsync(
|
||||
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
|
||||
null
|
||||
);
|
||||
const deviceId = persistedDeviceId ?? uuidv4();
|
||||
if (!persistedDeviceId) {
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
|
||||
}
|
||||
return new RudderstackAnalytics(deviceId);
|
||||
}
|
||||
|
||||
class NoOpAnalytics implements AnalyticsWithOrchestration {
|
||||
logEvent(): void {}
|
||||
setActor(): void {}
|
||||
async flushAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
const RudderstackAnalyticsConfig =
|
||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||
? {
|
||||
// staging environment
|
||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
}
|
||||
: {
|
||||
// prod environment
|
||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||
};
|
||||
|
||||
class RudderstackAnalytics implements AnalyticsWithOrchestration {
|
||||
private readonly rudderstackClient = new RudderAnalytics(
|
||||
RudderstackAnalyticsConfig.rudderstackWriteKey,
|
||||
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
|
||||
private identifiedActor: Actor | null = null;
|
||||
|
||||
constructor(private readonly persistentDeviceId: string) {
|
||||
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
|
||||
// both so that they can be associated
|
||||
this.rudderstackClient.identify({
|
||||
anonymousId: persistentDeviceId,
|
||||
});
|
||||
}
|
||||
|
||||
public setActor(actor: Actor): void {
|
||||
if (this.identifiedActor) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rudderstackClient.identify({
|
||||
userId: actor.id,
|
||||
anonymousId: this.persistentDeviceId,
|
||||
traits: {
|
||||
username: getActorDisplayName(actor),
|
||||
user_id: actor.id,
|
||||
user_type: actor.__typename,
|
||||
},
|
||||
});
|
||||
this.identifiedActor = actor;
|
||||
}
|
||||
|
||||
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
|
||||
const userId = this.identifiedActor?.id;
|
||||
const deviceId = this.persistentDeviceId;
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
this.rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: this.getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
public async flushAsync(): Promise<void> {
|
||||
await this.rudderstackClient.flush();
|
||||
}
|
||||
|
||||
private getRudderStackContext(): Record<string, any> {
|
||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||
return {
|
||||
os: { name: platform, version: os.release() },
|
||||
device: { type: platform, model: platform },
|
||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
|
||||
|
||||
export async function withAnalyticsAsync<Result>(
|
||||
analytics: Analytics,
|
||||
fn: () => Promise<Result>,
|
||||
{
|
||||
attemptEvent,
|
||||
successEvent,
|
||||
failureEvent,
|
||||
properties,
|
||||
}: {
|
||||
attemptEvent: AnalyticsEvent;
|
||||
successEvent: AnalyticsEvent;
|
||||
failureEvent: AnalyticsEvent;
|
||||
properties: AnalyticsEventProperties;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
analytics.logEvent(attemptEvent, properties);
|
||||
const result = await fn();
|
||||
analytics.logEvent(successEvent, properties);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
analytics.logEvent(failureEvent, {
|
||||
...properties,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+24
-88
@@ -1,106 +1,42 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
import got, { NormalizedOptions } from 'got';
|
||||
|
||||
import { ApiV2Error } from './ApiV2Error';
|
||||
import fetch, { RequestError, RequestInit } from './fetch';
|
||||
import { getAccessToken, getSessionSecret } from './user/User';
|
||||
|
||||
interface RequestOptions {
|
||||
body: JSONValue;
|
||||
}
|
||||
|
||||
export class ApiV2Client {
|
||||
constructor(
|
||||
private readonly authInfo: {
|
||||
accessToken: string | null;
|
||||
sessionSecret: string | null;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async putAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await this.requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
|
||||
}
|
||||
|
||||
public async postAsync(path: string, options: RequestOptions): Promise<any> {
|
||||
return await this.requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
|
||||
}
|
||||
|
||||
public async deleteAsync(path: string): Promise<any> {
|
||||
return await this.requestAsync(path, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
public async getAsync(path: string): Promise<any> {
|
||||
return await this.requestAsync(path, { method: 'GET' });
|
||||
}
|
||||
|
||||
private getAuthHeaders(): Record<string, string> {
|
||||
const token = this.authInfo.accessToken;
|
||||
if (token) {
|
||||
return { authorization: `Bearer ${token}` };
|
||||
}
|
||||
const sessionSecret = this.authInfo.sessionSecret;
|
||||
if (sessionSecret) {
|
||||
return { 'expo-session': sessionSecret };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
private async requestAsync(path: string, options: RequestInit): Promise<any> {
|
||||
try {
|
||||
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...this.getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
return await response.json();
|
||||
} catch (err) {
|
||||
await ApiV2Client.handleApiErrorAsync(err);
|
||||
}
|
||||
}
|
||||
|
||||
private static async handleApiErrorAsync(err: any): Promise<void> {
|
||||
if (err instanceof RequestError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = (await err.response.json()) as { [key: string]: any };
|
||||
} catch {
|
||||
throw new Error(`Malformed api response: ${await err.response.text()}`);
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
throw new ApiV2Error(result.errors[0]);
|
||||
}
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
export const apiClient = got.extend({
|
||||
prefixUrl: getExpoApiBaseUrl() + '/--/api/v2/',
|
||||
hooks: {
|
||||
beforeRequest: [
|
||||
(options: NormalizedOptions) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
options.headers.authorization = `Bearer ${token}`;
|
||||
return;
|
||||
}
|
||||
const sessionSecret = getSessionSecret();
|
||||
if (sessionSecret) {
|
||||
options.headers['expo-session'] = sessionSecret;
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
export function getExpoApiBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging-api.expo.dev`;
|
||||
return `https://staging.exp.host`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://127.0.0.1:3000`;
|
||||
} else {
|
||||
return `https://api.expo.dev`;
|
||||
return `https://exp.host`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getExpoWebsiteBaseUrl(): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return `https://staging.expo.dev`;
|
||||
return `https://staging.expo.io`;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return `http://expo.test`;
|
||||
} else {
|
||||
return `https://expo.dev`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return new URL(projectId, `https://staging-u.expo.dev`).href;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
return new URL(`expo-updates/${projectId}`, `http://127.0.0.1:3000`).href;
|
||||
} else {
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
return `https://expo.io`;
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranch,
|
||||
UpdateBranchFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import Log from '../log';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { BranchNotFoundError } from './utils';
|
||||
|
||||
export const BRANCHES_LIMIT = 50;
|
||||
|
||||
export async function selectBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
promptTitle,
|
||||
displayTextForListItem,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
|
||||
promptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<UpdateBranchFragment> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a branch in non-interactive mode.');
|
||||
}
|
||||
|
||||
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
|
||||
promptOptions: {
|
||||
title: promptTitle,
|
||||
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
|
||||
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
|
||||
},
|
||||
});
|
||||
if (!selectedBranch) {
|
||||
throw new Error(`Could not find any branches for project "${projectId}"`);
|
||||
}
|
||||
return selectedBranch;
|
||||
}
|
||||
|
||||
export async function listAndRenderBranchesOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesOnProjectAsync(
|
||||
graphqlClient,
|
||||
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
paginatedQueryOptions.offset,
|
||||
projectId
|
||||
);
|
||||
renderPageOfBranches(branches, paginatedQueryOptions);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
|
||||
promptOptions: {
|
||||
title: 'Load more branches?',
|
||||
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function queryBranchesOnProjectAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
limit: number,
|
||||
offset: number,
|
||||
projectId: string
|
||||
): Promise<UpdateBranchFragment[]> {
|
||||
return await BranchQuery.listBranchesOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
}
|
||||
|
||||
function renderPageOfBranches(
|
||||
currentPage: UpdateBranchFragment[],
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(currentPage);
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Branches:'));
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
currentPage
|
||||
.map(branch => formatBranch(getBranchDescription(branch)))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
||||
if (!newBranch) {
|
||||
throw new Error(`Could not create branch ${name}.`);
|
||||
}
|
||||
return newBranch;
|
||||
}
|
||||
|
||||
export async function ensureBranchExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { getVcsClient } from '../vcs';
|
||||
|
||||
export async function getDefaultBranchNameAsync(): Promise<string> {
|
||||
return (
|
||||
(await getVcsClient().getBranchNameAsync()) ||
|
||||
`branch-${Math.random().toString(36).substring(2, 4)}`
|
||||
);
|
||||
}
|
||||
|
||||
export class BranchNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Branch not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
|
||||
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { confirmAsync, selectAsync } from '../../prompts';
|
||||
|
||||
jest.mock('../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
jest.mock('../../prompts', () => {
|
||||
return {
|
||||
selectAsync: jest.fn(),
|
||||
confirmAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../graphql/queries/BuildQuery', () => {
|
||||
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
|
||||
return {
|
||||
BuildQuery: {
|
||||
...actual.BuildQuery,
|
||||
viewBuildsOnAppAsync: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe(selectBuildToCancelAsync.name, () => {
|
||||
const selectedBuildId = uuid();
|
||||
const projectId = uuid();
|
||||
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.mocked(BuildQuery.viewBuildsOnAppAsync)
|
||||
.mockImplementation(async () => [
|
||||
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not return build id when confirmation is rejected', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
||||
});
|
||||
|
||||
it('returns build id when confirmation is confirmed', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
||||
selectedBuildId
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function createMockBuildFragment({
|
||||
projectId,
|
||||
buildId,
|
||||
}: {
|
||||
projectId: string;
|
||||
buildId?: string;
|
||||
}): BuildFragment {
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
priority: BuildPriority.Normal,
|
||||
project: {
|
||||
__typename: 'App',
|
||||
slug: 'test-project',
|
||||
id: projectId,
|
||||
name: 'test-project',
|
||||
ownerAccount: {
|
||||
__typename: 'Account',
|
||||
id: uuid(),
|
||||
name: 'test-account',
|
||||
},
|
||||
},
|
||||
status: BuildStatus.InQueue,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { CredentialsSource, Workflow } from '@eas/config';
|
||||
import prompts from 'prompts';
|
||||
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { CredentialsProvider } from '../../credentials/CredentialsProvider';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
|
||||
jest.mock('prompts');
|
||||
|
||||
function createMockCredentialsProvider({
|
||||
hasRemote,
|
||||
hasLocal,
|
||||
isLocalSynced,
|
||||
}: any): CredentialsProvider {
|
||||
return {
|
||||
platform: 'android',
|
||||
hasRemoteAsync: jest.fn().mockImplementation(() => hasRemote || false),
|
||||
hasLocalAsync: jest.fn().mockImplementation(() => hasLocal || false),
|
||||
isLocalSyncedAsync: jest.fn().mockImplementation(() => isLocalSynced || false),
|
||||
} as CredentialsProvider;
|
||||
}
|
||||
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleWarn = console.warn;
|
||||
beforeAll(() => {
|
||||
console.log = jest.fn();
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
afterAll(() => {
|
||||
console.log = originalConsoleLog;
|
||||
console.warn = originalConsoleWarn;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
asMock(prompts).mockReset();
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
throw new Error(`unhandled prompts call - this shouldn't happen - fix tests!`);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureCredentialsAsync, () => {
|
||||
describe('for generic builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask which credentials to use when local and remote crednitals have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'local' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it('should ask when local and remote are not the same (select remote)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { select: 'remote' };
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should should throw an error when local and remote are not the same (in non interactive mode)', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Contents of your local credentials.json for Android are not the same as credentials on Expo servers'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('should ask when no credentials are present and return remote if confirm=true', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: true }; //confirm
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should ask when no credentials are present and abort if confimr=false', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementationOnce(() => {
|
||||
return { value: false }; //confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, false);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
'Aborting build process, credentials are not configured for Android'
|
||||
);
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it('should throw an error when no credentials are present in non interactive mode', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: false,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
asMock(prompts).mockImplementation(() => {
|
||||
return { value: true }; // confirm
|
||||
});
|
||||
|
||||
try {
|
||||
await ensureCredentialsAsync(provider, Workflow.Generic, CredentialsSource.AUTO, true);
|
||||
throw new Error('ensureCredentialsAsync should throw an Error');
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch('Credentials for this app are not configured');
|
||||
}
|
||||
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('for managed builds', () => {
|
||||
it("should use credentials.json if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: false,
|
||||
hasLocal: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
it("should use credentials from server if it's the only available credentials", async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
it('should use credentials.json if credentials are the same in credentials.json and on server', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: true,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
|
||||
it('should use credentials.json even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Managed,
|
||||
CredentialsSource.AUTO,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to local', () => {
|
||||
it('should use credentials.json without asking even if credentials on server are different', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.LOCAL,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('local');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generic builds with credentialsSource set to remote', () => {
|
||||
it('should use credentials from server even if credentials.json exists and have different values', async () => {
|
||||
const provider = createMockCredentialsProvider({
|
||||
hasRemote: true,
|
||||
hasLocal: true,
|
||||
isLocalSynced: false,
|
||||
});
|
||||
const src = await ensureCredentialsAsync(
|
||||
provider,
|
||||
Workflow.Generic,
|
||||
CredentialsSource.REMOTE,
|
||||
false
|
||||
);
|
||||
expect(prompts).toHaveBeenCalledTimes(0);
|
||||
expect(src).toBe('remote');
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,14 +0,0 @@
|
||||
import { truncateGitCommitMessage } from '../metadata';
|
||||
|
||||
describe(truncateGitCommitMessage, () => {
|
||||
it('returns undefined if no message was passed', () => {
|
||||
expect(truncateGitCommitMessage(undefined)).toBeUndefined();
|
||||
});
|
||||
it('returns commit message', () => {
|
||||
expect(truncateGitCommitMessage('a'.repeat(10))).toBe('a'.repeat(10));
|
||||
});
|
||||
|
||||
it('truncates long commit messages', () => {
|
||||
expect(truncateGitCommitMessage('a'.repeat(5000))).toBe(`${'a'.repeat(4093)}...`);
|
||||
});
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { ExitError } from '@oclif/core/lib/errors';
|
||||
import path from 'path';
|
||||
import { instance, mock, when } from 'ts-mockito';
|
||||
|
||||
import { CommonContext } from '../context';
|
||||
import { validatePNGsForManagedProjectAsync } from '../validate';
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
|
||||
describe(validatePNGsForManagedProjectAsync, () => {
|
||||
it('does not validate PNGs for generic projects', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
it('does not validate PNGs for projects with SDK >= 47', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '47.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
describe(Platform.ANDROID, () => {
|
||||
it('exits if foregroundImage is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if foregroundImage is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if foregroundImage is a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
xdescribe(Platform.IOS, () => {
|
||||
it('exits if icon is not a file with .png extension', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon is not a png file', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('exits if icon has alpha channel (transparency)', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
||||
});
|
||||
|
||||
it('does not throw if icon is a png file and does not have alpha channel', async () => {
|
||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
||||
when(ctxMock.exp).thenReturn({
|
||||
name: 'blah',
|
||||
slug: 'blah',
|
||||
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
|
||||
sdkVersion: '46.0.0',
|
||||
});
|
||||
const ctx = instance(ctxMock);
|
||||
|
||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { AndroidConfig, ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import log from '../../log';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const { username } = await ensureLoggedInAsync();
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
|
||||
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
|
||||
|
||||
await fs.writeFile(
|
||||
buildGradlePath,
|
||||
`${buildGradleContent}\n// Integration with Expo updates\n${gradleScriptApply}\n`
|
||||
);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, username)) {
|
||||
const result = await AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, username);
|
||||
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const { username } = await ensureLoggedInAsync();
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
let androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
|
||||
}
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, username)) {
|
||||
log.warn(
|
||||
'Native project configuration is not synced with values present in your app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradle(projectDir);
|
||||
const buildGradleContent = await fs.readFile(buildGradlePath, 'utf8');
|
||||
|
||||
if (!AndroidConfig.Updates.isBuildGradleConfigured(buildGradleContent, projectDir, exp)) {
|
||||
const gradleScriptApply = AndroidConfig.Updates.formatApplyLineForBuildGradle(projectDir, exp);
|
||||
throw new Error(`Missing ${gradleScriptApply} in ${buildGradlePath}`);
|
||||
}
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
const androidManifest = await AndroidConfig.Manifest.readAndroidManifestAsync(
|
||||
androidManifestPath
|
||||
);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
}
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
it('throws an informative error when multiple flavors are detected in the android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('throws an informative error when multiple flavor dimensions are defined in an android project', async () => {
|
||||
await initProjectWithGradleFileAsync(
|
||||
/*buildGradlePath*/ path.join(
|
||||
__dirname,
|
||||
'../../../project/android/__tests__/fixtures/multiflavor-with-dimensions-build.gradle'
|
||||
)
|
||||
);
|
||||
await expect(
|
||||
bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/multiflavor',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrow(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
});
|
||||
it('bumps expo.android.versionCode and buildGradle versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const nativeVersionCode = 100; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionCode: nativeVersionCode });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(124);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(124);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/app');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
|
||||
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.APP_VERSION', async () => {
|
||||
const nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
|
||||
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/app');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('3.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(123);
|
||||
expect(appJSON.expo.version).toBe('3.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(123);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync('/app');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.android.versionCode when strategy = BumpStrategy.VERSION_CODE', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(127);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(127);
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.1');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/app',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
expect(appJSON.expo.android.versionCode).toBe(126);
|
||||
});
|
||||
});
|
||||
|
||||
describe(maybeResolveVersionsAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/app',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('3.0.0');
|
||||
expect(appBuildVersion).toBe('123');
|
||||
});
|
||||
});
|
||||
describe('managed project', () => {
|
||||
it('reads the versions from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/app',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
expect(appVersion).toBe('5.0.0');
|
||||
expect(appBuildVersion).toBe('126');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
versionCode?: number;
|
||||
versionName?: string;
|
||||
} = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '3.0.0',
|
||||
android: {
|
||||
versionCode: 123,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '5.0.0',
|
||||
android: {
|
||||
versionCode: 126,
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/app'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
async function initProjectWithGradleFileAsync(gradleFilePath: string): Promise<void> {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'android/app/build.gradle': await fsReal.readFile(gradleFilePath, 'utf-8'),
|
||||
'./app.json': JSON.stringify({
|
||||
expo: {} as any,
|
||||
}),
|
||||
},
|
||||
'/multiflavor'
|
||||
);
|
||||
}
|
||||
@@ -1,155 +1,63 @@
|
||||
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
import { EasConfig, Workflow } from '@eas/config';
|
||||
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { transformJob } from './graphql';
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import { CredentialsResult, startBuildForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
import { resolveRemoteVersionCodeAsync } from './version';
|
||||
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
export async function startAndroidBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig,
|
||||
projectId: string
|
||||
): Promise<string> {
|
||||
const buildCtx = createBuildContext<Platform.Android>({
|
||||
commandCtx,
|
||||
platform: Platform.Android,
|
||||
easConfig,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You're building your Android app for internal distribution. However, we've detected that the Gradle command you defined (${chalk.underline(
|
||||
buildProfile.gradleCommand
|
||||
)}) includes string 'bundle'.
|
||||
This means that it will most likely produce an AAB and you will not be able to install it on your Android devices straight from the Expo website.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error('Update eas.json and come back again.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
const versionCodeOverride =
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
|
||||
projectDir: ctx.projectDir,
|
||||
projectId: ctx.projectId,
|
||||
exp: ctx.exp,
|
||||
applicationId,
|
||||
buildProfile,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return { applicationId, gradleContext, versionCodeOverride };
|
||||
}
|
||||
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.ANDROID>) => {
|
||||
return await ensureAndroidCredentialsAsync(ctx);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync(ctx.graphqlClient, {
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
localAutoIncrement:
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, jobData);
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
return await startBuildForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {},
|
||||
ensureCredentialsAsync: ensureAndroidCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {},
|
||||
prepareJobAsync,
|
||||
});
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
|
||||
return !ctx.buildProfile.withoutCredentials;
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.Android>): boolean {
|
||||
return (
|
||||
ctx.buildProfile.workflow === Workflow.Managed ||
|
||||
(ctx.buildProfile.workflow === Workflow.Generic && !ctx.buildProfile.withoutCredentials)
|
||||
);
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
ctx: BuildContext<Platform.Android>
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
androidApplicationIdentifier,
|
||||
const provider = new AndroidCredentialsProvider(
|
||||
await createCredentialsContextAsync(ctx.commandCtx.projectDir, {}),
|
||||
{
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
},
|
||||
});
|
||||
const { credentialsSource } = ctx.buildProfile;
|
||||
logCredentialsSource(credentialsSource, Platform.ANDROID);
|
||||
{ nonInteractive: ctx.commandCtx.nonInteractive }
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
ctx.buildProfile.workflow,
|
||||
ctx.buildProfile.credentialsSource,
|
||||
ctx.commandCtx.nonInteractive
|
||||
);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AndroidConfig } from '@expo/config';
|
||||
|
||||
import log from '../../log';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync } from './UpdatesModule';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
return;
|
||||
}
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await gitAddAsync(easGradlePath, { intentToAdd: true });
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('Configured the Android project');
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
|
||||
gradleCommand: job.gradleCommand,
|
||||
applicationArchivePath: job.applicationArchivePath,
|
||||
buildArtifactPaths: job.buildArtifactPaths,
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
function transformBuildType(buildType: Android.BuildType): AndroidBuildType {
|
||||
if (buildType === Android.BuildType.APK) {
|
||||
return AndroidBuildType.Apk;
|
||||
} else {
|
||||
return AndroidBuildType.AppBundle;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +1,50 @@
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { AndroidGenericBuildProfile, AndroidManagedBuildProfile, Workflow } from '@eas/config';
|
||||
import { Android, BuildType, Job, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { readSecretEnvsAsync } from '../../credentials/credentialsJson/read';
|
||||
import { gitRootDirectoryAsync } from '../../utils/git';
|
||||
import { BuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
|
||||
interface JobData {
|
||||
projectArchive: ArchiveSource;
|
||||
archiveUrl: string;
|
||||
credentials?: AndroidCredentials;
|
||||
}
|
||||
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
if (ctx.buildProfile.workflow === Workflow.Generic) {
|
||||
const partialJob = await prepareGenericJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else if (ctx.buildProfile.workflow === Workflow.Managed) {
|
||||
const partialJob = await prepareManagedJobAsync(ctx, jobData, ctx.buildProfile);
|
||||
return sanitizeJob(partialJob);
|
||||
} else {
|
||||
throw new Error("Unknown workflow. Shouldn't happen");
|
||||
}
|
||||
}
|
||||
|
||||
interface CommonJobProperties {
|
||||
platform: Platform.Android;
|
||||
projectUrl: string;
|
||||
secrets: {
|
||||
buildCredentials?: {
|
||||
keystore: Android.Keystore;
|
||||
};
|
||||
secretEnvs?: Record<string, string>;
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareJobCommonAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData
|
||||
): Promise<Partial<CommonJobProperties>> {
|
||||
const secretEnvs = await readSecretEnvsAsync(ctx.commandCtx.projectDir);
|
||||
const credentials = jobData.credentials;
|
||||
const buildCredentials = credentials
|
||||
? {
|
||||
buildCredentials: {
|
||||
@@ -41,49 +58,40 @@ export async function prepareJobAsync(
|
||||
}
|
||||
: {};
|
||||
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
projectRootDirectory,
|
||||
projectArchive: jobData.projectArchive,
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
return {
|
||||
platform: Platform.Android,
|
||||
projectUrl: jobData.archiveUrl,
|
||||
secrets: {
|
||||
...(secretEnvs ? { secretEnvs } : {}),
|
||||
...buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
buildType,
|
||||
username,
|
||||
...(ctx.android.versionCodeOverride && {
|
||||
version: {
|
||||
versionCode: ctx.android.versionCodeOverride,
|
||||
},
|
||||
}),
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return sanitizeJob(job);
|
||||
async function prepareGenericJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidGenericBuildProfile
|
||||
): Promise<Partial<Android.GenericJob>> {
|
||||
const projectRootDirectory = path.relative(await gitRootDirectoryAsync(), process.cwd()) || '.';
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: BuildType.Generic,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
projectRootDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareManagedJobAsync(
|
||||
ctx: BuildContext<Platform.Android>,
|
||||
jobData: JobData,
|
||||
buildProfile: AndroidManagedBuildProfile
|
||||
): Promise<Partial<Android.ManagedJob>> {
|
||||
return {
|
||||
...(await prepareJobCommonAsync(ctx, jobData)),
|
||||
type: BuildType.Managed,
|
||||
packageJson: { example: 'packageJson' },
|
||||
manifest: { example: 'manifest' },
|
||||
};
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log from '../../log';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectDir,
|
||||
exp,
|
||||
localAutoIncrement,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
localAutoIncrement?: AndroidVersionAutoIncrement;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: AndroidVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'versionCode') {
|
||||
return BumpStrategy.VERSION_CODE;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove this after a few months
|
||||
export async function cleanUpOldEasBuildGradleScriptAsync(projectDir: string): Promise<void> {
|
||||
const easBuildGradlePath = path.join(projectDir, 'android', 'app', 'eas-build.gradle');
|
||||
if (await fs.pathExists(easBuildGradlePath)) {
|
||||
Log.withTick(`Removing ${chalk.bold('eas-build.gradle')} as it's not longer necessary`);
|
||||
await fs.remove(easBuildGradlePath);
|
||||
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
const buildGradleContents = await fs.readFile(buildGradlePath, 'utf-8');
|
||||
const buildGradleContentsWithoutApply = buildGradleContents.replace(
|
||||
/apply from: ["'].\/eas-build.gradle["']\n/,
|
||||
''
|
||||
);
|
||||
if (buildGradleContentsWithoutApply !== buildGradleContents) {
|
||||
await fs.writeFile(buildGradlePath, buildGradleContentsWithoutApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig, Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import {
|
||||
getAppBuildGradleAsync,
|
||||
parseGradleCommand,
|
||||
resolveConfigValue,
|
||||
} from '../../project/android/gradleUtils';
|
||||
import { getNextVersionCode } from '../../project/android/versions';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
VERSION_CODE,
|
||||
NOOP,
|
||||
}
|
||||
|
||||
export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
const isMultiFlavor =
|
||||
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
|
||||
if (isMultiFlavor) {
|
||||
throw new Error(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
);
|
||||
}
|
||||
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await updateNativeVersionsAsync({
|
||||
projectDir,
|
||||
version: exp.version,
|
||||
versionCode: exp.android?.versionCode,
|
||||
});
|
||||
Log.log('Synchronized versions with build gradle');
|
||||
}
|
||||
|
||||
export async function bumpVersionInAppJsonAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
}: {
|
||||
bumpStrategy: BumpStrategy;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
|
||||
ensureStaticConfigExists(projectDir);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
if (bumpStrategy === BumpStrategy.APP_VERSION) {
|
||||
const appVersion = AndroidConfig.Version.getVersionName(exp) ?? '1.0.0';
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const versionCode = AndroidConfig.Version.getVersionCode(exp);
|
||||
const bumpedVersionCode = getNextVersionCode(versionCode);
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
|
||||
versionCode
|
||||
)} to ${chalk.bold(bumpedVersionCode)}`
|
||||
);
|
||||
await updateAppJsonConfigAsync({ projectDir, exp }, config => {
|
||||
config.android = { ...config.android, versionCode: bumpedVersionCode };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
try {
|
||||
const parsedGradleCommand = buildProfile.gradleCommand
|
||||
? parseGradleCommand(buildProfile.gradleCommand, buildGradle)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
appVersion:
|
||||
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
appBuildVersion: String(AndroidConfig.Version.getVersionCode(exp)),
|
||||
appVersion: exp.version,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateNativeVersionsAsync({
|
||||
projectDir,
|
||||
version,
|
||||
versionCode,
|
||||
}: {
|
||||
projectDir: string;
|
||||
version?: string;
|
||||
versionCode?: number;
|
||||
}): Promise<void> {
|
||||
const buildGradle = await readBuildGradleAsync(projectDir);
|
||||
if (!buildGradle) {
|
||||
throw new Error('This project is missing a build.gradle file.');
|
||||
}
|
||||
let updatedBuildGradle = buildGradle;
|
||||
if (version !== undefined) {
|
||||
updatedBuildGradle = updatedBuildGradle.replace(
|
||||
new RegExp(`versionName ".*"`),
|
||||
`versionName "${version}"`
|
||||
);
|
||||
}
|
||||
if (versionCode !== undefined) {
|
||||
updatedBuildGradle = updatedBuildGradle.replace(
|
||||
new RegExp(`versionCode.*`),
|
||||
`versionCode ${versionCode}`
|
||||
);
|
||||
}
|
||||
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
|
||||
}
|
||||
|
||||
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
if (!(await fs.pathExists(buildGradlePath))) {
|
||||
return undefined;
|
||||
}
|
||||
return await fs.readFile(buildGradlePath, 'utf8');
|
||||
}
|
||||
|
||||
async function writeBuildGradleAsync({
|
||||
projectDir,
|
||||
buildGradle,
|
||||
}: {
|
||||
projectDir: string;
|
||||
buildGradle: string;
|
||||
}): Promise<void> {
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
await fs.writeFile(buildGradlePath, buildGradle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns buildNumber that will be used for the next build. If current build profile
|
||||
* has an 'autoIncrement' option set, it increments the version on server.
|
||||
*/
|
||||
export async function resolveRemoteVersionCodeAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
applicationId,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
applicationId: string;
|
||||
buildProfile: BuildProfile<Platform.ANDROID>;
|
||||
}
|
||||
): Promise<string> {
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
AppPlatform.Android,
|
||||
applicationId
|
||||
);
|
||||
|
||||
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
|
||||
let currentBuildVersion: string;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localVersions.appBuildVersion) {
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
|
||||
);
|
||||
currentBuildVersion = localVersions.appBuildVersion;
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
);
|
||||
throw new Error('Remote versions are not configured.');
|
||||
}
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
const spinner = ora(
|
||||
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
try {
|
||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
platform: AppPlatform.Android,
|
||||
applicationIdentifier: applicationId,
|
||||
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
||||
buildVersion: currentBuildVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
|
||||
});
|
||||
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to initialize versionCode with ${chalk.bold(currentBuildVersion)}.`);
|
||||
throw err;
|
||||
}
|
||||
return currentBuildVersion;
|
||||
} else {
|
||||
const nextBuildVersion = getNextVersionCode(currentBuildVersion);
|
||||
const spinner = ora(
|
||||
`Incrementing versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
).start();
|
||||
try {
|
||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
platform: AppPlatform.Android,
|
||||
applicationIdentifier: applicationId,
|
||||
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
||||
buildVersion: String(nextBuildVersion),
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
|
||||
});
|
||||
spinner.succeed(
|
||||
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
);
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
`Failed to increment versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
return String(nextBuildVersion);
|
||||
}
|
||||
}
|
||||
@@ -1,194 +1,103 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import cliProgress from 'cli-progress';
|
||||
import { CredentialsSource } from '@eas/config';
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildParamsInput,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore, link } from '../log';
|
||||
import { Ora, ora } from '../ora';
|
||||
import {
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { apiClient } from '../api';
|
||||
import log from '../log';
|
||||
import { UploadType, uploadAsync } from '../uploads';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { platformDisplayNames } from './constants';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
import { collectMetadata } from './metadata';
|
||||
import { AnalyticsEvent, Platform, TrackingContext } from './types';
|
||||
import Analytics from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
import { makeProjectTarballAsync } from './utils/repository';
|
||||
|
||||
export interface CredentialsResult<Credentials> {
|
||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||
credentials: Credentials;
|
||||
}
|
||||
|
||||
export interface JobData<Credentials> {
|
||||
credentials?: Credentials;
|
||||
projectArchive: ArchiveSource;
|
||||
}
|
||||
|
||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
interface Builder<TPlatform extends Platform, Credentials, ProjectConfiguration> {
|
||||
ctx: BuildContext<TPlatform>;
|
||||
projectConfiguration: ProjectConfiguration;
|
||||
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): 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,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
prepareJobAsync(
|
||||
ctx: BuildContext<TPlatform>,
|
||||
jobData: {
|
||||
archiveUrl: string;
|
||||
credentials?: Credentials;
|
||||
projectConfiguration?: ProjectConfiguration;
|
||||
}
|
||||
): Promise<Job>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
|
||||
return {
|
||||
resourceClass: ctx.resourceClass,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
export async function startBuildForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
TJob extends Job
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
ProjectConfiguration
|
||||
>(builder: Builder<TPlatform, Credentials, ProjectConfiguration>): Promise<string> {
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
async () => await builder.ensureCredentialsAsync(builder.ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
successEvent: AnalyticsEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
|
||||
await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.syncProjectConfigurationAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
await reviewAndCommitChangesAsync(
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
if (!builder.ctx.commandCtx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(builder.ctx), {
|
||||
successEvent: AnalyticsEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
const projectArchive = ctx.localBuildOptions.enable
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
const archiveUrl = await uploadProjectAsync(builder.ctx);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const buildParams = resolveBuildParamsInput(ctx);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
const metadata = await collectMetadata(builder.ctx, {
|
||||
credentialsSource: credentialsResult?.source,
|
||||
});
|
||||
const job = await builder.prepareJobAsync(builder.ctx, {
|
||||
archiveUrl,
|
||||
credentials: credentialsResult?.credentials,
|
||||
projectConfiguration: builder.projectConfiguration,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (ctx.localBuildOptions.enable) {
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
async () => {
|
||||
log(`Starting ${platformDisplayNames[job.platform]} build`);
|
||||
const {
|
||||
data: { buildId, deprecationInfo },
|
||||
} = await apiClient
|
||||
.post(`projects/${builder.ctx.projectId}/builds`, {
|
||||
json: {
|
||||
job,
|
||||
metadata,
|
||||
},
|
||||
})
|
||||
.json();
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return buildId;
|
||||
},
|
||||
{
|
||||
successEvent: AnalyticsEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: builder.ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.code === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
log.error('EAS Build API changed, upgrade to latest expo-cli');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed. Upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build is down for maintenance. Try again later. Check ${link(
|
||||
'https://status.expo.dev/'
|
||||
)} for updates.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_IOS'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for iOS and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for Android and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||
) {
|
||||
Log.error(
|
||||
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (error?.graphQLErrors) {
|
||||
Log.error(
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
@@ -197,48 +106,21 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
let projectTarballPath;
|
||||
try {
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`Compressing project files and uploading to EAS Build. ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`
|
||||
);
|
||||
const projectTarball = await makeProjectTarballAsync();
|
||||
|
||||
if (projectTarball.size > 1024 * 1024 * 100) {
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
projectTarball.size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildGcsProjectSources,
|
||||
log('Uploading project to AWS S3');
|
||||
return await uploadAsync(
|
||||
UploadType.TURTLE_PROJECT_SOURCES,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
})
|
||||
createProgressTracker(projectTarball.size)
|
||||
);
|
||||
return bucketKey;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
successEvent: AnalyticsEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: AnalyticsEvent.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
@@ -248,284 +130,23 @@ 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,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
}
|
||||
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata,
|
||||
buildParams
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||
{ intervalSec = 10 } = {}
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
let spinner;
|
||||
let originalSpinnerText;
|
||||
if (buildIds.length === 1) {
|
||||
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
originalSpinnerText = 'Waiting for build to complete.';
|
||||
spinner = ora(originalSpinnerText).start();
|
||||
} else {
|
||||
originalSpinnerText = 'Waiting for builds to complete. You can press Ctrl+C to exit.';
|
||||
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
|
||||
async function withAnalyticsAsync<Result>(
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
successEvent: AnalyticsEvent;
|
||||
failureEvent: AnalyticsEvent;
|
||||
trackingCtx: TrackingContext;
|
||||
}
|
||||
while (true) {
|
||||
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
||||
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
|
||||
if (!refetch) {
|
||||
return builds;
|
||||
}
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuildsSafelyAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
buildIds: string[]
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
const promises = buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(graphqlClient, buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
return await Promise.all(promises);
|
||||
}
|
||||
|
||||
interface BuildProgressResult {
|
||||
refetch: boolean;
|
||||
}
|
||||
|
||||
let queueProgressBarStarted = false;
|
||||
const queueProgressBar = new cliProgress.SingleBar(
|
||||
{ format: '|{bar}| {estimatedWaitTime}' },
|
||||
cliProgress.Presets.rect
|
||||
);
|
||||
|
||||
async function handleSingleBuildProgressAsync(
|
||||
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
if (build === null) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
if (queueProgressBarStarted && build?.status && build.status !== BuildStatus.InQueue) {
|
||||
if (build.status === BuildStatus.InProgress) {
|
||||
queueProgressBar.update(queueProgressBar.getTotal(), {
|
||||
estimatedWaitTime: '',
|
||||
});
|
||||
}
|
||||
queueProgressBar.stop();
|
||||
Log.newLine();
|
||||
queueProgressBarStarted = false;
|
||||
spinner.start('Build is about to start');
|
||||
}
|
||||
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
case BuildStatus.InQueue: {
|
||||
spinner.text = 'Build queued...';
|
||||
const progressBarPayload =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? { estimatedWaitTime: formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds) }
|
||||
: { estimatedWaitTime: '' };
|
||||
|
||||
if (
|
||||
!queueProgressBarStarted &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
spinner.stopAndPersist();
|
||||
if (build.priority !== BuildPriority.High) {
|
||||
Log.newLine();
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${chalk.underline(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
|
||||
queueProgressBar.start(
|
||||
build.initialQueuePosition + 1,
|
||||
build.initialQueuePosition - build.queuePosition + 1,
|
||||
progressBarPayload
|
||||
);
|
||||
queueProgressBarStarted = true;
|
||||
}
|
||||
if (typeof build.queuePosition === 'number') {
|
||||
queueProgressBar.update(build.queuePosition, progressBarPayload);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case BuildStatus.Canceled:
|
||||
spinner.fail('Build canceled');
|
||||
return { refetch: false };
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return { refetch: false };
|
||||
} else {
|
||||
throw new Error('Standalone build failed!');
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
return { refetch: true };
|
||||
}
|
||||
|
||||
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
|
||||
[BuildPriority.Normal]: 'queue',
|
||||
[BuildPriority.NormalPlus]: 'queue',
|
||||
[BuildPriority.High]: 'priority queue',
|
||||
};
|
||||
|
||||
const statusToDisplayName: Record<BuildStatus, string> = {
|
||||
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
|
||||
[BuildStatus.InQueue]: 'in queue',
|
||||
[BuildStatus.InProgress]: 'in progress',
|
||||
[BuildStatus.Canceled]: 'canceled',
|
||||
[BuildStatus.Finished]: 'finished',
|
||||
[BuildStatus.Errored]: 'failed',
|
||||
};
|
||||
|
||||
const platforms = [AppPlatform.Android, AppPlatform.Ios];
|
||||
|
||||
async function handleMultipleBuildsProgressAsync(
|
||||
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
|
||||
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string }
|
||||
): Promise<BuildProgressResult> {
|
||||
const buildCount = maybeBuilds.length;
|
||||
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
|
||||
|
||||
const allFinished =
|
||||
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
|
||||
const allSettled =
|
||||
builds.filter(build =>
|
||||
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
|
||||
).length === buildCount;
|
||||
|
||||
if (allSettled) {
|
||||
if (allFinished) {
|
||||
spinner.succeed(formatSettledBuildsText(builds));
|
||||
} else {
|
||||
spinner.fail(formatSettledBuildsText(builds));
|
||||
}
|
||||
return { refetch: false };
|
||||
} else {
|
||||
spinner.text = formatPendingBuildsText(originalSpinnerText, builds);
|
||||
return { refetch: true };
|
||||
}
|
||||
}
|
||||
|
||||
function formatSettledBuildsText(builds: BuildFragment[]): string {
|
||||
return platforms
|
||||
.map(platform => {
|
||||
const build = nullthrows(
|
||||
builds.find(build => build.platform === platform),
|
||||
`Build for platform ${platform} must be defined in this context`
|
||||
);
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(statusToDisplayName[build.status])}`;
|
||||
})
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string {
|
||||
return [
|
||||
originalSpinnerText,
|
||||
...platforms.map(platform => {
|
||||
const build = builds.find(build => build.platform === platform);
|
||||
const status = build ? statusToDisplayName[build.status] : 'unknown';
|
||||
let extraInfo = '';
|
||||
if (
|
||||
build?.status === BuildStatus.InQueue &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
) {
|
||||
const percent = Math.floor(
|
||||
((build.initialQueuePosition - build.queuePosition + 1) /
|
||||
(build.initialQueuePosition + 1)) *
|
||||
100
|
||||
);
|
||||
const estimatedWaitTime =
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
? ` - ${formatEstimatedWaitTime(build.estimatedWaitTimeLeftSeconds)}`
|
||||
: '';
|
||||
extraInfo = ` - queue progress: ${chalk.bold(`${percent}%`)}${estimatedWaitTime}`;
|
||||
}
|
||||
return `${appPlatformEmojis[platform]} ${
|
||||
appPlatformDisplayNames[platform]
|
||||
} build - status: ${chalk.bold(status)}${extraInfo}`;
|
||||
}),
|
||||
].join('\n ');
|
||||
}
|
||||
|
||||
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
|
||||
return maybeBuild !== null;
|
||||
}
|
||||
|
||||
function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
|
||||
if (estimatedWaitTimeLeftSeconds < 5 * 60) {
|
||||
return 'starting soon...';
|
||||
} else {
|
||||
const n = Math.floor(estimatedWaitTimeLeftSeconds / (10 * 60)) + 1;
|
||||
return `starting in about ${n}0 minutes...`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/settings/subscriptions`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -1,106 +1,82 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import { getConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../log';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
import log from '../log';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { gitAddAsync } from '../utils/git';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { BuildCommandPlatform } from './types';
|
||||
import {
|
||||
ensureGitRepoExistsAsync,
|
||||
ensureGitStatusIsCleanAsync,
|
||||
modifyAndCommitAsync,
|
||||
} from './utils/repository';
|
||||
|
||||
interface ConfigureParams {
|
||||
export async function configureAsync(options: {
|
||||
platform: BuildCommandPlatform;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync();
|
||||
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
const ctx: ConfigureContext = {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
shouldConfigureAndroid: [BuildCommandPlatform.ALL, BuildCommandPlatform.ANDROID].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [BuildCommandPlatform.ALL, BuildCommandPlatform.IOS].includes(
|
||||
options.platform
|
||||
),
|
||||
// TODO: better detection of native projects
|
||||
hasAndroidNativeProject: await fs.pathExists(path.join(options.projectDir, 'android')),
|
||||
hasIosNativeProject: await fs.pathExists(path.join(options.projectDir, 'ios')),
|
||||
};
|
||||
await modifyAndCommitAsync(
|
||||
async () => {
|
||||
await ensureEasJsonExistsAsync(ctx);
|
||||
if (ctx.shouldConfigureAndroid) {
|
||||
await configureAndroidAsync(ctx);
|
||||
}
|
||||
if (ctx.shouldConfigureIos) {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
},
|
||||
{
|
||||
commitMessage: 'Configure EAS Build',
|
||||
nonInteractive: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates eas.json if it does not exist.
|
||||
*
|
||||
* Returns:
|
||||
* - false - if eas.json already exists
|
||||
* - true - if eas.json was created by the function
|
||||
*/
|
||||
export async function ensureProjectConfiguredAsync(
|
||||
configureParams: ConfigureParams
|
||||
): Promise<boolean> {
|
||||
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) {
|
||||
return false;
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = path.join(ctx.projectDir, 'eas.json');
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await configureAsync(configureParams);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
await createEasJsonAsync(projectDir);
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
distribution: 'internal',
|
||||
const easJson = {
|
||||
builds: {
|
||||
android: {
|
||||
gradleCommand: ':app:assembleDebug',
|
||||
release: {
|
||||
workflow: ctx.hasAndroidNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
release: {
|
||||
workflow: ctx.hasIosNativeProject ? 'generic' : 'managed',
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const easJson =
|
||||
hasAndroidNativeProject || hasIosNativeProject
|
||||
? EAS_JSON_BARE_DEFAULT
|
||||
: EAS_JSON_MANAGED_DEFAULT;
|
||||
};
|
||||
|
||||
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||
await getVcsClient().trackFileAsync(easJsonPath);
|
||||
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
|
||||
await gitAddAsync(easJsonPath, { intentToAdd: true });
|
||||
log.withTick('Created eas.json file');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
export const platformDisplayNames = {
|
||||
[Platform.iOS]: 'iOS',
|
||||
[Platform.Android]: 'Android',
|
||||
};
|
||||
@@ -1,56 +1,112 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, EasConfig, iOSBuildProfile } from '@eas/config';
|
||||
import { ExpoConfig, getConfig } from '@expo/config';
|
||||
|
||||
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
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';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { getProjectAccountNameAsync } from '../project/projectUtils';
|
||||
import { User } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { BuildCommandPlatform, Platform, TrackingContext } from './types';
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
versionCodeOverride?: string;
|
||||
export interface CommandContext {
|
||||
requestedPlatform: BuildCommandPlatform;
|
||||
profile: string;
|
||||
projectDir: string;
|
||||
user: User;
|
||||
accountName: string;
|
||||
projectName: string;
|
||||
exp: ExpoConfig;
|
||||
trackingCtx: TrackingContext;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}
|
||||
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
buildNumberOverride?: string;
|
||||
export async function createCommandContextAsync({
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
trackingCtx,
|
||||
nonInteractive = false,
|
||||
skipCredentialsCheck = false,
|
||||
skipProjectConfiguration = false,
|
||||
waitForBuildEnd,
|
||||
}: {
|
||||
requestedPlatform: BuildCommandPlatform;
|
||||
profile: string;
|
||||
projectDir: string;
|
||||
trackingCtx: TrackingContext;
|
||||
nonInteractive: boolean;
|
||||
skipCredentialsCheck: boolean;
|
||||
skipProjectConfiguration: boolean;
|
||||
waitForBuildEnd: boolean;
|
||||
}): Promise<CommandContext> {
|
||||
const user = await ensureLoggedInAsync();
|
||||
const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true });
|
||||
const accountName = await getProjectAccountNameAsync(projectDir);
|
||||
const projectName = exp.slug;
|
||||
|
||||
return {
|
||||
requestedPlatform,
|
||||
profile,
|
||||
projectDir,
|
||||
user,
|
||||
accountName,
|
||||
projectName,
|
||||
exp,
|
||||
trackingCtx,
|
||||
nonInteractive,
|
||||
skipCredentialsCheck,
|
||||
skipProjectConfiguration,
|
||||
waitForBuildEnd,
|
||||
};
|
||||
}
|
||||
|
||||
export interface ConfigureContext {
|
||||
user: User;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
hasAndroidNativeProject: boolean;
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
type PlatformBuildProfile<T extends Platform> = T extends Platform.Android
|
||||
? AndroidBuildProfile
|
||||
: iOSBuildProfile;
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountName: string;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
buildProfile: BuildProfile<T>;
|
||||
buildProfileName: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
clearCache: boolean;
|
||||
credentialsCtx: CredentialsContext;
|
||||
exp: ExpoConfig;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
noWait: boolean;
|
||||
runFromCI: boolean;
|
||||
commandCtx: CommandContext;
|
||||
trackingCtx: TrackingContext;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
message?: string;
|
||||
analyticsEventProperties: AnalyticsEventProperties;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
buildProfile: PlatformBuildProfile<T>;
|
||||
}
|
||||
|
||||
export function createBuildContext<T extends Platform>({
|
||||
platform,
|
||||
easConfig,
|
||||
commandCtx,
|
||||
projectId,
|
||||
}: {
|
||||
platform: T;
|
||||
easConfig: EasConfig;
|
||||
commandCtx: CommandContext;
|
||||
projectId: string;
|
||||
}): BuildContext<T> {
|
||||
const buildProfile = easConfig.builds[platform] as PlatformBuildProfile<T> | undefined;
|
||||
if (!buildProfile) {
|
||||
throw new Error(`${platform} build profile does not exist`);
|
||||
}
|
||||
const builderTrackingCtx = {
|
||||
...commandCtx.trackingCtx,
|
||||
platform,
|
||||
};
|
||||
return {
|
||||
commandCtx,
|
||||
trackingCtx: builderTrackingCtx,
|
||||
platform,
|
||||
buildProfile,
|
||||
projectId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { EasJsonReader } from '@eas/config';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
|
||||
import { apiClient } from '../api';
|
||||
import log from '../log';
|
||||
import { ensureProjectExistsAsync } from '../project/ensureProjectExists';
|
||||
import { sleep } from '../utils/promise';
|
||||
import { startAndroidBuildAsync } from './android/build';
|
||||
import { CommandContext } from './context';
|
||||
import { startIosBuildAsync } from './ios/build';
|
||||
import { Build, BuildCommandPlatform, BuildStatus } from './types';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureGitRepoExistsAsync, ensureGitStatusIsCleanAsync } from './utils/repository';
|
||||
|
||||
export async function buildAsync(commandCtx: CommandContext): Promise<void> {
|
||||
await ensureGitRepoExistsAsync();
|
||||
await ensureGitStatusIsCleanAsync();
|
||||
|
||||
const projectId = await ensureProjectExistsAsync({
|
||||
accountName: commandCtx.accountName,
|
||||
projectName: commandCtx.projectName,
|
||||
privacy: commandCtx.exp.privacy,
|
||||
});
|
||||
|
||||
const scheduledBuilds = await startBuildsAsync(commandCtx, projectId);
|
||||
log.newLine();
|
||||
await printLogsUrls(commandCtx.accountName, scheduledBuilds);
|
||||
log.newLine();
|
||||
|
||||
if (commandCtx.waitForBuildEnd) {
|
||||
const builds = await waitForBuildEndAsync(
|
||||
commandCtx,
|
||||
projectId,
|
||||
scheduledBuilds.map(i => i.buildId)
|
||||
);
|
||||
printBuildResults(builds);
|
||||
}
|
||||
}
|
||||
|
||||
async function startBuildsAsync(
|
||||
commandCtx: CommandContext,
|
||||
projectId: string
|
||||
): Promise<
|
||||
{ platform: BuildCommandPlatform.ANDROID | BuildCommandPlatform.IOS; buildId: string }[]
|
||||
> {
|
||||
const shouldBuildAndroid = [BuildCommandPlatform.ANDROID, BuildCommandPlatform.ALL].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const shouldBuildiOS = [BuildCommandPlatform.IOS, BuildCommandPlatform.ALL].includes(
|
||||
commandCtx.requestedPlatform
|
||||
);
|
||||
const easConfig = await new EasJsonReader(
|
||||
commandCtx.projectDir,
|
||||
commandCtx.requestedPlatform
|
||||
).readAsync(commandCtx.profile);
|
||||
|
||||
const scheduledBuilds: {
|
||||
platform: BuildCommandPlatform.ANDROID | BuildCommandPlatform.IOS;
|
||||
buildId: string;
|
||||
}[] = [];
|
||||
if (shouldBuildAndroid) {
|
||||
const buildId = await startAndroidBuildAsync(commandCtx, easConfig, projectId);
|
||||
scheduledBuilds.push({ platform: BuildCommandPlatform.ANDROID, buildId });
|
||||
}
|
||||
if (shouldBuildiOS) {
|
||||
const buildId = await startIosBuildAsync(commandCtx, easConfig, projectId);
|
||||
scheduledBuilds.push({ platform: BuildCommandPlatform.IOS, buildId });
|
||||
}
|
||||
return scheduledBuilds;
|
||||
}
|
||||
|
||||
async function waitForBuildEndAsync(
|
||||
commandCtx: CommandContext,
|
||||
projectId: string,
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 1800, intervalSec = 30 } = {}
|
||||
): Promise<(Build | null)[]> {
|
||||
log('Waiting for build to complete. You can press Ctrl+C to exit.');
|
||||
const spinner = ora().start();
|
||||
let time = new Date().getTime();
|
||||
const endTime = time + timeoutSec * 1000;
|
||||
while (time <= endTime) {
|
||||
const builds: (Build | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
const { data } = await apiClient.get(`projects/${projectId}/builds/${buildId}`).json();
|
||||
return data;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
switch (builds[0]?.status) {
|
||||
case BuildStatus.FINISHED:
|
||||
spinner.succeed('Build finished.');
|
||||
return builds;
|
||||
case BuildStatus.IN_QUEUE:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
case BuildStatus.IN_PROGRESS:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.ERRORED:
|
||||
spinner.fail('Build failed.');
|
||||
throw new Error(`Standalone build failed!`);
|
||||
default:
|
||||
spinner.warn('Unknown status.');
|
||||
throw new Error(`Unknown status: ${builds} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.FINISHED).length === builds.length) {
|
||||
spinner.succeed('All build have finished.');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status ? [BuildStatus.FINISHED, BuildStatus.ERRORED].includes(build.status) : false
|
||||
).length === builds.length
|
||||
) {
|
||||
spinner.fail('Some of the builds failed.');
|
||||
return builds;
|
||||
} else {
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.IN_QUEUE).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.IN_PROGRESS).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.ERRORED).length;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.FINISHED).length;
|
||||
const unknownState = builds.length - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknownState && chalk.red(`Builds in unknown state: ${unknownState}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleep(intervalSec * 1000);
|
||||
}
|
||||
spinner.warn('Timed out.');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import getenv from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { Actor } from '../user/User';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
easJsonCliConfig,
|
||||
clearCache = false,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
noWait,
|
||||
platform,
|
||||
projectDir,
|
||||
resourceClass,
|
||||
message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
clearCache: boolean;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
nonInteractive: boolean;
|
||||
noWait: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = account.id;
|
||||
const runFromCI = getenv.boolish('CI', false);
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
projectInfo: { exp, projectId },
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const analyticsEventProperties = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
|
||||
...(accountId && { account_id: accountId }),
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
no_wait: noWait,
|
||||
run_from_ci: runFromCI,
|
||||
};
|
||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName: account.name,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
resourceClass,
|
||||
easJsonCliConfig,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
noWait,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
analyticsEventProperties,
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
workflow,
|
||||
message,
|
||||
runFromCI,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
return {
|
||||
...common,
|
||||
android: await createAndroidContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
} else {
|
||||
const common = commonContext as CommonContext<Platform.IOS>;
|
||||
return {
|
||||
...common,
|
||||
ios: await createIosContextAsync(common),
|
||||
} as BuildContext<T>;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
}): Partial<AnalyticsEventProperties> {
|
||||
let includesDevClient;
|
||||
const version = tryGetDevClientVersion(projectDir);
|
||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.gradleCommand?.includes('Debug'));
|
||||
} else if (platform === Platform.IOS && 'buildConfiguration' in buildProfile) {
|
||||
includesDevClient = Boolean(version && buildProfile.buildConfiguration === 'Debug');
|
||||
} else if (buildProfile.developmentClient) {
|
||||
includesDevClient = true;
|
||||
} else {
|
||||
includesDevClient = false;
|
||||
}
|
||||
|
||||
if (version) {
|
||||
return { dev_client: includesDevClient, dev_client_version: version };
|
||||
} else {
|
||||
return { dev_client: includesDevClient };
|
||||
}
|
||||
}
|
||||
|
||||
function tryGetDevClientVersion(projectDir: string): string | null {
|
||||
try {
|
||||
const pkg = JsonFile.read(resolveFrom(projectDir, 'expo-dev-client/package.json'));
|
||||
return pkg.version?.toString() ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { CredentialsSource, Workflow } from '@eas/config';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { CredentialsProvider } from '../credentials/CredentialsProvider';
|
||||
import log from '../log';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { platformDisplayNames } from './constants';
|
||||
|
||||
const USING_CREDENTIALS_JSON_MSG = 'Using credentials from the local credentials.json file';
|
||||
const USING_REMOTE_CREDENTIALS_MSG = 'Using credentials stored on the Expo servers';
|
||||
|
||||
async function ensureCredentialsAutoAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
const platform = platformDisplayNames[provider.platform];
|
||||
switch (workflow) {
|
||||
case Workflow.Managed:
|
||||
if (await provider.hasLocalAsync()) {
|
||||
return CredentialsSource.LOCAL;
|
||||
} else {
|
||||
return CredentialsSource.REMOTE;
|
||||
}
|
||||
case Workflow.Generic: {
|
||||
const hasLocal = await provider.hasLocalAsync();
|
||||
const hasRemote = await provider.hasRemoteAsync();
|
||||
if (hasRemote && hasLocal) {
|
||||
if (!(await provider.isLocalSyncedAsync())) {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers. To use the desired credentials, set the "builds.${platform}.{profile}.credentialsSource" field in the credentials.json file to one of the following: "local", "remote".`
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
`Contents of your local credentials.json for ${platform} are not the same as credentials on Expo servers`
|
||||
);
|
||||
}
|
||||
|
||||
const { select } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
message: 'Which credentials you want to use for this build?',
|
||||
choices: [
|
||||
{ title: 'Local credentials.json', value: CredentialsSource.LOCAL },
|
||||
{ title: 'Credentials stored on Expo servers.', value: CredentialsSource.REMOTE },
|
||||
],
|
||||
});
|
||||
return select;
|
||||
} else {
|
||||
return CredentialsSource.LOCAL;
|
||||
}
|
||||
} else if (hasLocal) {
|
||||
log(chalk.bold(USING_CREDENTIALS_JSON_MSG));
|
||||
return CredentialsSource.LOCAL;
|
||||
} else if (hasRemote) {
|
||||
log(chalk.bold(USING_REMOTE_CREDENTIALS_MSG));
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}. Either configure credentials.json, or launch the build without "--non-interactive" flag to get a prompt to generate credentials automatically.`
|
||||
);
|
||||
} else {
|
||||
log.warn(
|
||||
`Credentials for this app are not configured and there is no entry in credentials.json for ${platform}`
|
||||
);
|
||||
}
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Do you want to generate new credentials?',
|
||||
});
|
||||
if (confirm) {
|
||||
return CredentialsSource.REMOTE;
|
||||
} else {
|
||||
throw new Error(`Aborting build process, credentials are not configured for ${platform}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCredentialsAsync(
|
||||
provider: CredentialsProvider,
|
||||
workflow: Workflow,
|
||||
src: CredentialsSource,
|
||||
nonInteractive: boolean
|
||||
): Promise<CredentialsSource.LOCAL | CredentialsSource.REMOTE> {
|
||||
switch (src) {
|
||||
case CredentialsSource.LOCAL:
|
||||
log(chalk.bold(USING_CREDENTIALS_JSON_MSG));
|
||||
return CredentialsSource.LOCAL;
|
||||
case CredentialsSource.REMOTE:
|
||||
log(chalk.bold(USING_REMOTE_CREDENTIALS_MSG));
|
||||
return CredentialsSource.REMOTE;
|
||||
case CredentialsSource.AUTO:
|
||||
log('Resolving credentials source (auto mode)');
|
||||
return await ensureCredentialsAutoAsync(provider, workflow, nonInteractive);
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
|
||||
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
|
||||
if (archiveSource.type === ArchiveSourceType.S3) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.S3,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.GCS) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Gcs,
|
||||
bucketKey: archiveSource.bucketKey,
|
||||
};
|
||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||
return {
|
||||
type: ProjectArchiveSourceType.Url,
|
||||
url: archiveSource.url,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported project archive source type: '${archiveSource.type}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
workflow: metadata.workflow && transformWorkflow(metadata.workflow),
|
||||
iosEnterpriseProvisioning:
|
||||
metadata.iosEnterpriseProvisioning &&
|
||||
transformIosEnterpriseProvisioning(metadata.iosEnterpriseProvisioning),
|
||||
};
|
||||
}
|
||||
|
||||
function transformCredentialsSource(
|
||||
credentialsSource: Metadata['credentialsSource']
|
||||
): BuildCredentialsSource {
|
||||
if (credentialsSource === 'local') {
|
||||
return BuildCredentialsSource.Local;
|
||||
} else {
|
||||
return BuildCredentialsSource.Remote;
|
||||
}
|
||||
}
|
||||
|
||||
function transformDistribution(distribution: Metadata['distribution']): DistributionType {
|
||||
if (distribution === 'internal') {
|
||||
return DistributionType.Internal;
|
||||
} else if (distribution === 'simulator') {
|
||||
return DistributionType.Simulator;
|
||||
} else {
|
||||
return DistributionType.Store;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformWorkflow(workflow: Workflow): BuildWorkflow {
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
return BuildWorkflow.Generic;
|
||||
} else {
|
||||
return BuildWorkflow.Managed;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformIosEnterpriseProvisioning(
|
||||
enterpriseProvisioning: Metadata['iosEnterpriseProvisioning']
|
||||
): BuildIosEnterpriseProvisioning {
|
||||
if (enterpriseProvisioning === 'adhoc') {
|
||||
return BuildIosEnterpriseProvisioning.Adhoc;
|
||||
} else {
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { ExpoConfig, IOSConfig } from '@expo/config';
|
||||
import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import log from '../../log';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { gitAddAsync } from '../../utils/git';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const { username } = await ensureLoggedInAsync();
|
||||
let xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
|
||||
xcodeProject = IOSConfig.Updates.ensureBundleReactNativePhaseContainsConfigurationScript(
|
||||
projectDir,
|
||||
exp,
|
||||
xcodeProject
|
||||
);
|
||||
await fs.writeFile(IOSConfig.Paths.getPBXProjectPath(projectDir), xcodeProject.writeSync());
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, username)) {
|
||||
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, username);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
// TODO: ensure ExpoPlist in pbxproj
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const { username } = await ensureLoggedInAsync();
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir, exp);
|
||||
} catch (error) {
|
||||
log.error(
|
||||
'expo-updates module is not configured. Please run "eas build:configure" first to configure the project'
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistVersionConfigurationSynced(exp, expoPlist)) {
|
||||
expoPlist = IOSConfig.Updates.setVersionsConfig(exp, expoPlist);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, username)) {
|
||||
log.warn(
|
||||
'Native project configuration is not synced with values present in you app.json, run "eas build:configure" to make sure all values are applied in the native project'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
const xcodeProject = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
|
||||
if (!IOSConfig.Updates.isShellScriptBuildPhaseConfigured(projectDir, exp, xcodeProject)) {
|
||||
const script = 'expo-updates/scripts/create-manifest-ios.sh';
|
||||
const buildPhase = '"Bundle React Native code and images"';
|
||||
throw new Error(`Path to ${script} is missing in a ${buildPhase} build phase.`);
|
||||
}
|
||||
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSet(expoPlist)) {
|
||||
throw new Error('Missing values in Expo.plist');
|
||||
}
|
||||
}
|
||||
|
||||
async function readExpoPlistAsync(projectDir: string): Promise<IOSConfig.ExpoPlist> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
|
||||
let expoPlist = {};
|
||||
if (await fs.pathExists(expoPlistPath)) {
|
||||
const expoPlistContent = await fs.readFile(expoPlistPath, 'utf8');
|
||||
expoPlist = plist.parse(expoPlistContent);
|
||||
}
|
||||
return expoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
projectDir: string,
|
||||
expoPlist: IOSConfig.ExpoPlist
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
const expoPlistContent = plist.build(expoPlist);
|
||||
|
||||
await fs.mkdirp(path.dirname(expoPlistPath));
|
||||
await fs.writeFile(expoPlistPath, expoPlistContent);
|
||||
await gitAddAsync(expoPlistPath, { intentToAdd: true });
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
evaluateTemplateString,
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
|
||||
IOSConfig.Target,
|
||||
'getXCBuildConfigurationFromPbxproj'
|
||||
);
|
||||
const getPbxproj = jest.spyOn(IOSConfig.XcodeUtils, 'getPbxproj');
|
||||
|
||||
afterAll(async () => {
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.remove(os.tmpdir());
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vol.reset();
|
||||
// do not remove the following line
|
||||
// this fixes a weird error with tempy in @expo/image-utils
|
||||
await fs.mkdirp(os.tmpdir());
|
||||
});
|
||||
|
||||
describe(evaluateTemplateString, () => {
|
||||
it('evaluates the template string when value is a number', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: 123 })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when value is a string', () => {
|
||||
expect(evaluateTemplateString('$(BLAH_BLAH)', { BLAH_BLAH: '123' })).toBe('123');
|
||||
});
|
||||
it('evaluates the template string when template is not the only element', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '123' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
it('evaluates the template string when template value is double quoted', () => {
|
||||
expect(evaluateTemplateString('before$(BLAH_BLAH)after', { BLAH_BLAH: '"123"' })).toBe(
|
||||
'before123after'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// bare workflow
|
||||
describe(bumpVersionAsync, () => {
|
||||
beforeEach(() => {
|
||||
getPbxproj.mockImplementation(() => ({} as any));
|
||||
});
|
||||
afterEach(() => {
|
||||
getXCBuildConfigurationFromPbxproj.mockReset();
|
||||
getPbxproj.mockReset();
|
||||
});
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
getXCBuildConfigurationFromPbxproj.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||
},
|
||||
} as XCBuildConfiguration)
|
||||
);
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
targets: [
|
||||
{
|
||||
targetName: 'example',
|
||||
bundleIdentifier: 'example.com',
|
||||
entitlements: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
|
||||
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
|
||||
|
||||
getXCBuildConfigurationFromPbxproj.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
||||
},
|
||||
} as XCBuildConfiguration)
|
||||
);
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
targets: [
|
||||
{
|
||||
targetName: 'example',
|
||||
bundleIdentifier: 'example.com',
|
||||
entitlements: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info2.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.APP_VERSION', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
getXCBuildConfigurationFromPbxproj.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||
},
|
||||
} as XCBuildConfiguration)
|
||||
);
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
targets: [
|
||||
{
|
||||
targetName: 'example',
|
||||
bundleIdentifier: 'example.com',
|
||||
entitlements: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.1');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initBareWorkflowProject();
|
||||
getXCBuildConfigurationFromPbxproj.mockImplementation(
|
||||
() =>
|
||||
({
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||
},
|
||||
} as XCBuildConfiguration)
|
||||
);
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
targets: [
|
||||
{
|
||||
targetName: 'example',
|
||||
bundleIdentifier: 'example.com',
|
||||
entitlements: {},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
const infoPlist = (await readPlistAsync(
|
||||
'/repo/ios/myproject/Info.plist'
|
||||
)) as IOSConfig.InfoPlist;
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
expect(infoPlist['CFBundleShortVersionString']).toBe('1.0.0');
|
||||
expect(infoPlist['CFBundleVersion']).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
// managed workflow
|
||||
describe(bumpVersionInAppJsonAsync, () => {
|
||||
it('bumps expo.ios.buildNumber when strategy = BumpStrategy.BUILD_NUMBER', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version when strategy = BumpStrategy.SHORT_VERSION', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.1');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.1');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
|
||||
it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
|
||||
const fakeExp = initManagedProject();
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('1.0.0');
|
||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||
expect(appJSON.expo.version).toBe('1.0.0');
|
||||
expect(appJSON.expo.ios.buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe(readBuildNumberAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the build number from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the build number from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||
expect(buildNumber).toBe('1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(readShortVersionAsync, () => {
|
||||
describe('bare project', () => {
|
||||
it('reads the short version from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
it('evaluates interpolated build number', async () => {
|
||||
const exp = initBareWorkflowProject({
|
||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||
});
|
||||
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||
});
|
||||
expect(buildNumber).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('managed project', () => {
|
||||
it('reads the version from app config', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||
expect(appVersion).toBe('1.0.0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(getInfoPlistPath, () => {
|
||||
it('returns default path if INFOPLIST_FILE is not specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {});
|
||||
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
|
||||
});
|
||||
it('returns INFOPLIST_FILE if specified', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates SRCROOT in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
it('evaluates BuildSettings in Info.plist', () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./ios/testapp/Info.plist': '',
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
const plistPath = getInfoPlistPath('/repo', {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
|
||||
TARGET_NAME: 'NotInfo',
|
||||
});
|
||||
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||
});
|
||||
});
|
||||
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
buildNumber = '1',
|
||||
infoPlistName = 'Info.plist',
|
||||
}: { appVersion?: string; buildNumber?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${appVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${buildNumber}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
|
||||
function initManagedProject(): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
version: '1.0.0',
|
||||
ios: {
|
||||
buildNumber: '1',
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./app.json': JSON.stringify({
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
}
|
||||
@@ -1,116 +1,77 @@
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
import { EasConfig, Workflow } from '@eas/config';
|
||||
import { IOSConfig } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
|
||||
import { IosCredentials } 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';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { startBuildForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommandContext, createBuildContext } from '../context';
|
||||
import { Platform } from '../types';
|
||||
import { syncProjectConfigurationAsync } from './configure';
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
import { resolveRemoteBuildNumberAsync } from './version';
|
||||
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile } = ctx;
|
||||
export async function startIosBuildAsync(
|
||||
commandCtx: CommandContext,
|
||||
easConfig: EasConfig,
|
||||
projectId: string
|
||||
): Promise<string> {
|
||||
const buildCtx = createBuildContext<Platform.iOS>({
|
||||
commandCtx,
|
||||
platform: Platform.iOS,
|
||||
easConfig,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
let iosNativeProjectScheme: string | undefined;
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
iosNativeProjectScheme = buildCtx.buildProfile.scheme ?? (await resolveSchemeAsync(buildCtx));
|
||||
}
|
||||
if (buildCtx.buildProfile.workflow === Workflow.Generic) {
|
||||
await syncProjectConfigurationAsync(commandCtx.projectDir, commandCtx.exp);
|
||||
}
|
||||
return await startBuildForPlatformAsync({
|
||||
ctx: buildCtx,
|
||||
projectConfiguration: {
|
||||
iosNativeProjectScheme,
|
||||
},
|
||||
ensureCredentialsAsync: ensureIosCredentialsAsync,
|
||||
ensureProjectConfiguredAsync: async () => {},
|
||||
prepareJobAsync,
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveSchemeAsync(ctx: BuildContext<Platform.iOS>): Promise<string> {
|
||||
const schemes = IOSConfig.Scheme.getSchemesFromXcodeproj(ctx.commandCtx.projectDir);
|
||||
if (schemes.length === 1) {
|
||||
return schemes[0];
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
nonInteractive: ctx.nonInteractive,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
buildProfile
|
||||
const sortedSchemes = sortBy(schemes);
|
||||
log.newLine();
|
||||
log(
|
||||
`We've found multiple schemes in your Xcode project: ${chalk.bold(sortedSchemes.join(', '))}`
|
||||
);
|
||||
const targets = await resolveTargetsAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
xcodeBuildContext,
|
||||
env: buildProfile.env,
|
||||
});
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const buildNumberOverride =
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? await resolveRemoteBuildNumberAsync(ctx.graphqlClient, {
|
||||
projectDir: ctx.projectDir,
|
||||
projectId: ctx.projectId,
|
||||
exp: ctx.exp,
|
||||
applicationTarget,
|
||||
buildProfile,
|
||||
})
|
||||
: undefined;
|
||||
return {
|
||||
bundleIdentifier: applicationTarget.bundleIdentifier,
|
||||
applicationTarget,
|
||||
targets,
|
||||
xcodeBuildContext,
|
||||
buildNumberOverride,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync(ctx.graphqlClient, {
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
targets: ctx.ios.targets,
|
||||
localAutoIncrement:
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
log(
|
||||
`You can specify the scheme you want to build at ${chalk.bold(
|
||||
'builds.ios.PROFILE_NAME.scheme'
|
||||
)} in eas.json.`
|
||||
);
|
||||
if (ctx.commandCtx.nonInteractive) {
|
||||
const withoutTvOS = sortedSchemes.filter(i => !i.includes('tvOS'));
|
||||
const scheme = withoutTvOS.length > 0 ? withoutTvOS[0] : sortedSchemes[0];
|
||||
log(`You've run Expo CLI in non-interactive mode, choosing the ${chalk.bold(scheme)} scheme.`);
|
||||
log.newLine();
|
||||
return scheme;
|
||||
} else {
|
||||
const { selectedScheme } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selectedScheme',
|
||||
message: 'Which scheme would you like to build now?',
|
||||
choices: sortedSchemes.map(scheme => ({ title: scheme, value: scheme })),
|
||||
});
|
||||
log.newLine();
|
||||
return selectedScheme as string;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ExpoConfig, IOSConfig } from '@expo/config';
|
||||
import chalk from 'chalk';
|
||||
import once from 'lodash/once';
|
||||
|
||||
import log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
export const getBundleIdentifier = once(_getBundleIdentifier);
|
||||
|
||||
async function _getBundleIdentifier(projectDir: string, manifest: ExpoConfig): Promise<string> {
|
||||
const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj(
|
||||
projectDir
|
||||
);
|
||||
const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(manifest);
|
||||
if (bundleIdentifierFromPbxproj !== null && bundleIdentifierFromConfig !== null) {
|
||||
if (bundleIdentifierFromPbxproj === bundleIdentifierFromConfig) {
|
||||
return bundleIdentifierFromPbxproj;
|
||||
} else {
|
||||
log.newLine();
|
||||
log.warn(
|
||||
`We detected that your Xcode project is configured with a different bundle identifier than the one defined in app.json/app.config.js.`
|
||||
);
|
||||
log(`If you choose the one defined in app.json/app.config.js we'll automatically configure your Xcode project with it.
|
||||
However, if you choose the one defined in the Xcode project you'll have to update app.json/app.config.js on your own.
|
||||
Otherwise, you'll see this prompt again in the future.`);
|
||||
log.newLine();
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'bundleIdentifier',
|
||||
message: 'Which bundle identifier should we use?',
|
||||
choices: [
|
||||
{
|
||||
title: `Defined in the Xcode project: ${chalk.bold(bundleIdentifierFromPbxproj)}`,
|
||||
value: bundleIdentifierFromPbxproj,
|
||||
},
|
||||
{
|
||||
title: `Defined in app.json/app.config.js: ${chalk.bold(bundleIdentifierFromConfig)}`,
|
||||
value: bundleIdentifierFromConfig,
|
||||
},
|
||||
],
|
||||
});
|
||||
return bundleIdentifier;
|
||||
}
|
||||
} else if (bundleIdentifierFromPbxproj === null && bundleIdentifierFromConfig === null) {
|
||||
throw new Error('Please define "expo.ios.bundleIdentifier" in app.json/app.config.js');
|
||||
} else {
|
||||
if (bundleIdentifierFromPbxproj !== null) {
|
||||
log(
|
||||
`Using ${chalk.bold(
|
||||
bundleIdentifierFromPbxproj
|
||||
)} as the bundle identifier (read from the Xcode project).`
|
||||
);
|
||||
return bundleIdentifierFromPbxproj;
|
||||
} else {
|
||||
// bundleIdentifierFromConfig is never null in this case
|
||||
// the following line is to satisfy TS
|
||||
const bundleIdentifier = bundleIdentifierFromConfig ?? '';
|
||||
log(
|
||||
`Using ${chalk.bold(
|
||||
bundleIdentifier
|
||||
)} as the bundle identifier (read from app.json/app.config.js).
|
||||
We'll automatically configure your Xcode project using this value.`
|
||||
);
|
||||
return bundleIdentifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { CredentialsSource, Workflow } from '@eas/config';
|
||||
import { ExpoConfig, IOSConfig } from '@expo/config';
|
||||
|
||||
import * as ProvisioningProfileUtils from '../../credentials/ios/utils/provisioningProfile';
|
||||
import log from '../../log';
|
||||
import { getProjectAccountNameAsync } from '../../project/projectUtils';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { getBundleIdentifier } from './bundleIdentifer';
|
||||
import { resolveIosCredentialsAsync } from './credentials';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
return;
|
||||
}
|
||||
const bundleIdentifier = await getBundleIdentifier(ctx.projectDir, ctx.exp);
|
||||
IOSConfig.BundleIdenitifer.setBundleIdentifierForPbxproj(ctx.projectDir, bundleIdentifier, false);
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: 'Do you want to configure credentials for the Xcode project?',
|
||||
});
|
||||
if (confirm) {
|
||||
await resolveCredentialsAndConfigureXcodeProjectAsync(ctx, bundleIdentifier);
|
||||
}
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
log.withTick('Configured the Xcode project.');
|
||||
}
|
||||
|
||||
export async function syncProjectConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
// TODO: check bundle identifier
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveCredentialsAndConfigureXcodeProjectAsync(
|
||||
ctx: ConfigureContext,
|
||||
bundleIdentifier: string
|
||||
): Promise<void> {
|
||||
const { credentials } = await resolveIosCredentialsAsync(
|
||||
ctx.projectDir,
|
||||
{
|
||||
app: {
|
||||
accountName: await getProjectAccountNameAsync(ctx.projectDir),
|
||||
projectName: ctx.exp.slug,
|
||||
bundleIdentifier,
|
||||
},
|
||||
workflow: Workflow.Generic,
|
||||
credentialsSource: CredentialsSource.AUTO,
|
||||
},
|
||||
{
|
||||
nonInteractive: false,
|
||||
}
|
||||
);
|
||||
|
||||
const profileName = ProvisioningProfileUtils.readProfileName(credentials.provisioningProfile);
|
||||
const appleTeam = ProvisioningProfileUtils.readAppleTeam(credentials.provisioningProfile);
|
||||
IOSConfig.ProvisioningProfile.setProvisioningProfileForPbxproj(ctx.projectDir, {
|
||||
profileName,
|
||||
appleTeamId: appleTeam.teamId,
|
||||
});
|
||||
// TODO: copy profile and add dist cert to keychain
|
||||
}
|
||||
@@ -1,36 +1,72 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, Workflow } from '@eas/config';
|
||||
import assert from 'assert';
|
||||
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { createCredentialsContextAsync } from '../../credentials/context';
|
||||
import IosCredentialsProvider, {
|
||||
IosCredentials,
|
||||
} from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { AppLookupParams } from '../../credentials/ios/credentials';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import { ensureCredentialsAsync } from '../credentials';
|
||||
import { Platform } from '../types';
|
||||
|
||||
export async function ensureIosCredentialsAsync(
|
||||
buildCtx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
ctx: BuildContext<Platform.iOS>
|
||||
): Promise<CredentialsResult<IosCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(buildCtx)) {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
}
|
||||
assert(ctx.commandCtx.exp?.ios?.bundleIdentifier, 'ios.bundleIdentifier is required');
|
||||
return await resolveIosCredentialsAsync(
|
||||
ctx.commandCtx.projectDir,
|
||||
{
|
||||
app: {
|
||||
accountName: ctx.commandCtx.accountName,
|
||||
projectName: ctx.commandCtx.projectName,
|
||||
bundleIdentifier: ctx.commandCtx.exp.ios.bundleIdentifier,
|
||||
},
|
||||
workflow: ctx.buildProfile.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
},
|
||||
{
|
||||
nonInteractive: ctx.commandCtx.nonInteractive,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
interface ResolveCredentialsParams {
|
||||
app: AppLookupParams;
|
||||
workflow: Workflow;
|
||||
credentialsSource: CredentialsSource;
|
||||
}
|
||||
|
||||
const { credentialsSource } = buildCtx.buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
export async function resolveIosCredentialsAsync(
|
||||
projectDir: string,
|
||||
params: ResolveCredentialsParams,
|
||||
options: { nonInteractive: boolean }
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(
|
||||
await createCredentialsContextAsync(projectDir, {}),
|
||||
params.app,
|
||||
{ nonInteractive: options.nonInteractive }
|
||||
);
|
||||
const credentialsSource = await ensureCredentialsAsync(
|
||||
provider,
|
||||
params.workflow,
|
||||
params.credentialsSource,
|
||||
options.nonInteractive
|
||||
);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
function shouldProvideCredentials(ctx: BuildContext<Platform.iOS>): boolean {
|
||||
return (
|
||||
(ctx.buildProfile.workflow === Workflow.Managed &&
|
||||
ctx.buildProfile.buildType !== 'simulator') ||
|
||||
ctx.buildProfile.workflow === Workflow.Generic
|
||||
);
|
||||
}
|
||||
Loaded 100 of 829 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user