mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0b3b1237 |
No files matched your search
@@ -11,6 +11,7 @@ module.exports = {
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
'flowtype/no-types-missing-file-annotation': 'off',
|
||||
curly: 'warn',
|
||||
'import/no-cycle': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
@@ -26,6 +27,10 @@ module.exports = {
|
||||
name: 'lodash',
|
||||
message: "Don't use lodash, it's heavy!",
|
||||
},
|
||||
{
|
||||
name: 'node-fetch',
|
||||
message: 'Use got instead, node-fetch is used only for the GraphQL client.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -1,5 +1 @@
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
|
||||
**/yarn.lock linguist-generated
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "\U0001F41B Bug Report in EAS CLI"
|
||||
labels: 'needs review'
|
||||
labels: 'bug'
|
||||
description: 'You want to report a reproducible bug or regression in EAS CLI.'
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -14,11 +14,6 @@ body:
|
||||
- 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
|
||||
@@ -26,18 +21,12 @@ body:
|
||||
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.
|
||||
placeholder: Run `eas diagnostics` and paste the output here
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: "\U0001F64B Enhancement"
|
||||
labels: 'enhancement,needs review'
|
||||
labels: 'enhancement'
|
||||
description: 'Want us to add something to EAS CLI?'
|
||||
body:
|
||||
- type: markdown
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
# 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.
|
||||
- [ ] I've tagged the changelog entry with `[EAS BUILD API]` if it's a part of a breaking change to EAS Build API (only possible when updating `@expo/eas-build-job` package).
|
||||
|
||||
# Why
|
||||
|
||||
|
||||
@@ -10,11 +10,9 @@ jobs:
|
||||
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@v1
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- name: Checkout Pull Request
|
||||
run: hub pr checkout ${{ github.event.issue.number }}
|
||||
env:
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
@@ -25,30 +25,22 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- 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 "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- id: arm
|
||||
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
|
||||
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,8 +48,8 @@ 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
|
||||
@@ -66,27 +58,22 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- 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 "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
||||
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
|
||||
@@ -94,29 +81,27 @@ jobs:
|
||||
needs: release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- 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 "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win
|
||||
- id: x86
|
||||
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||
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,7 +110,7 @@ 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:
|
||||
@@ -134,11 +119,11 @@ jobs:
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
@@ -147,25 +132,15 @@ jobs:
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
|
||||
echo "Run \"npm dist-tag add eas-cli@$VERSION latest-eas-build\" to promote it to production."
|
||||
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@v1
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
|
||||
@@ -7,14 +7,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: '16'
|
||||
- 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,15 +10,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['14', '16', '18']
|
||||
node: ['12', '14', '16']
|
||||
include:
|
||||
- node: '18'
|
||||
- node: '16'
|
||||
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
|
||||
@@ -27,14 +27,8 @@ jobs:
|
||||
if: ${{ !matrix.coverage }}
|
||||
- run: yarn test --coverage
|
||||
if: ${{ matrix.coverage }}
|
||||
env:
|
||||
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
||||
# Remove this whenever we solved those OOM errors.
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
- run: yarn lint --max-warnings=0
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
working-directory: ./scripts
|
||||
- run: yarn lint-changelog
|
||||
- run: shellcheck bin/*.sh
|
||||
- uses: codecov/codecov-action@v1
|
||||
if: ${{ matrix.coverage }}
|
||||
with:
|
||||
|
||||
+10
-17
@@ -1,21 +1,14 @@
|
||||
# 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
|
||||
.DS_Store
|
||||
+2
-3
@@ -2,8 +2,7 @@
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"bracketSameLine": true,
|
||||
"jsxBracketSameLine": true,
|
||||
"trailingComma": "es5",
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "auto"
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+40
-971
File diff suppressed because it is too large.
Load diff
+2
-11
@@ -1,16 +1,7 @@
|
||||
* @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-cli/src/submit @barthap @dsokal
|
||||
packages/eas-cli/src/commands/submit.ts @barthap @dsokal
|
||||
|
||||
packages/eas-json @dsokal @wkozyra95
|
||||
+9
-47
@@ -1,48 +1,10 @@
|
||||
# Development
|
||||
## Releasing a new version
|
||||
|
||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
|
||||
Open your `.zshrc` or other config file and add:
|
||||
|
||||
```
|
||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
||||
```
|
||||
|
||||
Then use it with `easd` like `easd build`.
|
||||
|
||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
||||
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
## Format
|
||||
|
||||
- Be sure to update the [`CHANGELOG.md`](./CHANGELOG.md) with changes for every PR. There is a changelog bot that can generate the proper entry for you. The instructions on how to use it are in the PR description placeholder.
|
||||
- End `async` functions with `Async` like `runAsync`. This is just how we format functions at Expo.
|
||||
- Utilize the unified `Log` module instead of `console.log`.
|
||||
|
||||
## Using local and staging API
|
||||
|
||||
For development against local API:
|
||||
|
||||
- Set up the API server as described at https://github.com/expo/universe/tree/main/server/www.
|
||||
- Set the `EXPO_LOCAL=1` env variable either on every command run (e.g. `EXPO_LOCAL=1 easd ...`) or more permamently with `export EXPO_LOCAL=1` (works in a current shell session).
|
||||
|
||||
For development against staging API:
|
||||
|
||||
- Set the `EXPO_STAGING=1` env variable either on every command run (e.g. `EXPO_STAGING=1 easd ...`) or more permamently with `export EXPO_STAGING=1` (works in a current shell session).
|
||||
|
||||
## Working on local builds (`eas build --local`)
|
||||
|
||||
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
|
||||
|
||||
## Testing
|
||||
|
||||
Run `yarn test` either in the repository root or in a package directory that you're working on.
|
||||
|
||||
## Releasing
|
||||
|
||||
See [RELEASING.md](./RELEASING.md).
|
||||
1. Run `yarn release` in the repository root folder.
|
||||
2. Wait, the `release` workflow on GitHub Actions is going to take care of the rest (including publishing packages to npm and updating the changelog).
|
||||
3. Check the changelog. If any of the entries have been tagged with `[EAS BUILD API]`
|
||||
- Open `package.json` and note the version of `@expo/eas-build-job` used in the eas-cli.
|
||||
- Find the `Publish` commit in [expo/eas-build](https://github.com/expo/eas-build/commits) for that version. In most cases, it will be the latest `Publish` commit.
|
||||
- Find the `eas-cli-local-build-plugin` version published in that commit.
|
||||

|
||||
- Run `npm dist-tags add eas-cli-local-build-plugin@VERSION latest` with the version from the previous step.
|
||||
@@ -1,18 +0,0 @@
|
||||
# Releasing EAS CLI
|
||||
|
||||
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.
|
||||
|
||||
## Choosing the next version
|
||||
|
||||
As stated above, the next EAS CLI version is chosen automatically based on the existing changelog entries under the unreleased section in **CHANGELOG.md**.
|
||||
|
||||
The algorithm works as follows:
|
||||
|
||||
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
|
||||
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
|
||||
- Otherwise, bump the PATCH version.
|
||||
|
||||
## Updating EAS CLI version on EAS Build
|
||||
|
||||
The release process promotes the new version to staging. After verifying it works there, you can promote it to production with `npm dist-tag add eas-cli@{version} latest-eas-build`.
|
||||
+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."
|
||||
}
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.6.1",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
"version": "0.34.1"
|
||||
}
|
||||
+19
-24
@@ -1,40 +1,35 @@
|
||||
{
|
||||
"name": "eas-cli-root",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"workspaces": ["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",
|
||||
"build:tarballs:linux": "yarn build && yarn workspace eas-cli oclif-dev pack --targets linux-x64",
|
||||
"typecheck": "lerna run typecheck",
|
||||
"start": "lerna run watch --stream --parallel",
|
||||
"start": "lerna run watch --parallel",
|
||||
"watch": "yarn start",
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "lerna run rebuild && ./scripts/bin/run release",
|
||||
"lint": "eslint . --ext .ts --ext .js",
|
||||
"release": "lerna version",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/node": "18.11.18",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-config-universe": "11.1.1",
|
||||
"eslint-plugin-async-protect": "3.0.0",
|
||||
"@expo/oclif-dev-cli": "1.24.0-expo",
|
||||
"@types/jest": "27.0.1",
|
||||
"@types/node": "16.7.8",
|
||||
"eslint": "7.32.0",
|
||||
"eslint-config-universe": "8.0.0",
|
||||
"eslint-plugin-async-protect": "1.1.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "29.3.1",
|
||||
"jest-watch-typeahead": "2.2.1",
|
||||
"lerna": "6.4.1",
|
||||
"oclif": "3.4.3",
|
||||
"prettier": "2.8.2",
|
||||
"ts-jest": "29.0.3",
|
||||
"typescript": "4.9.4"
|
||||
"jest": "27.1.0",
|
||||
"jest-watch-typeahead": "0.6.4",
|
||||
"lerna": "4.0.0",
|
||||
"prettier": "2.3.2",
|
||||
"ts-jest": "27.0.5",
|
||||
"typescript": "4.4.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
"yarn": "1.22.19"
|
||||
"node": "16.10.0",
|
||||
"yarn": "1.22.15"
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,6 @@ 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',
|
||||
{
|
||||
|
||||
+143
-965
File diff suppressed because it is too large.
Load diff
Regular → Executable
+6
-8
@@ -1,15 +1,13 @@
|
||||
const fs = require('fs/promises');
|
||||
#!/usr/bin/env sh
|
||||
|
||||
const path = './src/graphql/generated.ts';
|
||||
OUTFILE="src/graphql/generated.ts"
|
||||
|
||||
(async () => {
|
||||
const generatedCode = await fs.readFile(path, 'utf8');
|
||||
const comment = `/**
|
||||
content="/**
|
||||
* 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);
|
||||
})();
|
||||
printf '%s\n%s\n' "$content" "$(cat $OUTFILE)" > $OUTFILE
|
||||
@@ -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,12 +1,10 @@
|
||||
overwrite: true
|
||||
schema: 'https://staging-api.expo.dev/graphql'
|
||||
schema: 'https://staging.exp.host/--/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:
|
||||
@@ -16,7 +14,7 @@ generates:
|
||||
dedupeOperationSuffix: true
|
||||
hooks:
|
||||
afterOneFileWrite:
|
||||
- 'node ./scripts/annotate-graphql-codegen.js'
|
||||
- ./annotate-graphql-codegen.sh
|
||||
./graphql.schema.json:
|
||||
plugins:
|
||||
- 'introspection'
|
||||
Generated
+19535
-30593
File diff suppressed because it is too large.
Load diff
+76
-114
@@ -1,129 +1,110 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.6.1",
|
||||
"version": "0.34.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.37",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "7.0.3",
|
||||
"@expo/config-plugins": "5.0.4",
|
||||
"@expo/config-types": "47.0.0",
|
||||
"@expo/eas-build-job": "0.2.103",
|
||||
"@expo/eas-json": "3.6.1",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "0.0.57",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.20",
|
||||
"@expo/plugin-help": "5.1.22",
|
||||
"@expo/plugin-warn-if-update-available": "2.5.0",
|
||||
"@expo/prebuild-config": "5.0.7",
|
||||
"@expo/apple-utils": "0.0.0-alpha.26",
|
||||
"@expo/config": "6.0.3",
|
||||
"@expo/config-plugins": "4.0.3",
|
||||
"@expo/eas-build-job": "0.2.57",
|
||||
"@expo/eas-json": "^0.34.1",
|
||||
"@expo/json-file": "8.2.33",
|
||||
"@expo/pkcs12": "0.0.4",
|
||||
"@expo/plist": "0.0.14",
|
||||
"@expo/plugin-warn-if-update-available": "1.7.0",
|
||||
"@expo/prebuild-config": "3.0.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/rudder-sdk-node": "1.1.0",
|
||||
"@expo/sdk-runtime-versions": "1.0.0",
|
||||
"@expo/spawn-async": "1.5.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.23.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.10",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "3.1.1",
|
||||
"@urql/exchange-retry": "1.0.0",
|
||||
"ajv": "8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"@oclif/command": "1.8.0",
|
||||
"@oclif/config": "1.17.0",
|
||||
"@oclif/errors": "1.3.5",
|
||||
"@oclif/plugin-help": "3.2.3",
|
||||
"@urql/core": "2.3.1",
|
||||
"@urql/exchange-retry": "0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.11.2",
|
||||
"dateformat": "4.6.3",
|
||||
"dotenv": "16.0.3",
|
||||
"cli-table3": "0.6.0",
|
||||
"dateformat": "4.5.1",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.8.1",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.2.12",
|
||||
"fast-glob": "3.2.7",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "10.1.0",
|
||||
"form-data": "3.0.0",
|
||||
"fs-extra": "10.0.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.4",
|
||||
"got": "11.8.2",
|
||||
"gradle-to-js": "2.0.0",
|
||||
"graphql": "15.5.1",
|
||||
"graphql-tag": "2.12.5",
|
||||
"ignore": "5.1.8",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.7.0",
|
||||
"jks-js": "1.0.1",
|
||||
"joi": "17.4.2",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.1.2",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"mime": "2.5.2",
|
||||
"minimatch": "3.0.4",
|
||||
"nanoid": "3.1.25",
|
||||
"node-fetch": "2.6.1",
|
||||
"node-forge": "0.10.0",
|
||||
"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",
|
||||
"prompts": "2.3.2",
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.8",
|
||||
"semver": "7.3.5",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.1.13",
|
||||
"strip-ansi": "6.0.0",
|
||||
"tar": "6.1.11",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.1",
|
||||
"tslib": "2.3.1",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.0",
|
||||
"uuid": "8.3.2",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.16.3",
|
||||
"@graphql-codegen/introspection": "2.2.3",
|
||||
"@graphql-codegen/typescript": "2.8.7",
|
||||
"@graphql-codegen/typescript-operations": "2.5.12",
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@graphql-codegen/cli": "^1.19.2",
|
||||
"@graphql-codegen/introspection": "^1.18.0",
|
||||
"@graphql-codegen/typescript": "^1.17.11",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.10",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/fs-extra": "9.0.12",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.3.1",
|
||||
"@types/pngjs": "6.0.1",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.4.2",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "9.0.0",
|
||||
"@types/mime": "2.0.3",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/node-forge": "^0.9.5",
|
||||
"@types/prompts": "^2.0.9",
|
||||
"@types/semver": "7.3.8",
|
||||
"@types/tar": "4.0.5",
|
||||
"@types/tough-cookie": "4.0.1",
|
||||
"@types/uuid": "8.3.1",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"axios": "0.21.4",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.18.2",
|
||||
"memfs": "3.4.13",
|
||||
"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.9.4"
|
||||
"memfs": "3.2.2",
|
||||
"mockdate": "^3.0.2",
|
||||
"nock": "^13.0.5",
|
||||
"tempy": "0.7.0",
|
||||
"ts-deepmerge": "1.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/build",
|
||||
"/oclif.manifest.json",
|
||||
"/schema"
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"keywords": [
|
||||
@@ -136,16 +117,12 @@
|
||||
"bin": "eas",
|
||||
"commands": "./build/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-autocomplete",
|
||||
"@expo/plugin-help",
|
||||
"@oclif/plugin-help",
|
||||
"@expo/plugin-warn-if-update-available"
|
||||
],
|
||||
"topics": {
|
||||
"account": {
|
||||
"description": "manage account"
|
||||
},
|
||||
"branch": {
|
||||
"description": "manage update branches"
|
||||
"description": "manage your account"
|
||||
},
|
||||
"build": {
|
||||
"description": "build app binaries"
|
||||
@@ -154,13 +131,10 @@
|
||||
"description": "manage update channels"
|
||||
},
|
||||
"device": {
|
||||
"description": "manage Apple devices for Internal Distribution"
|
||||
},
|
||||
"metadata": {
|
||||
"description": "manage store configuration"
|
||||
"description": "manage your Apple devices for internal distribution"
|
||||
},
|
||||
"project": {
|
||||
"description": "manage project"
|
||||
"description": "manage your project"
|
||||
},
|
||||
"release": {
|
||||
"description": "manage update releases"
|
||||
@@ -177,11 +151,11 @@
|
||||
},
|
||||
"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"
|
||||
"message": "<%= chalk.bold('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available, please upgrade.') %>\n<%= chalk.dim('Proceeding with outdated version') %>\n"
|
||||
},
|
||||
"update": {
|
||||
"node": {
|
||||
"version": "18.6.0"
|
||||
"version": "12.13.0"
|
||||
},
|
||||
"s3": {
|
||||
"templates": {
|
||||
@@ -191,30 +165,18 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalHelpFlags": [
|
||||
"-h"
|
||||
],
|
||||
"additionalVersionFlags": [
|
||||
"-v"
|
||||
]
|
||||
}
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rimraf oclif.manifest.json",
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"postpack": "rm -f oclif.manifest.json",
|
||||
"prepack": "rm -rf build && yarn build && yarn oclif-dev manifest && yarn oclif-dev readme && node patch-readme",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"version": "yarn oclif-dev readme && node 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"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
"yarn": "1.22.19"
|
||||
"clean": "rm -rf dist build node_modules yarn-error.log"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
const fs = require('fs/promises');
|
||||
const { promises: fs } = require('fs');
|
||||
|
||||
(async () => {
|
||||
// Patch `oclif readme` path and link generation
|
||||
// Patch `oclif-dev 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');
|
||||
@@ -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-mdn.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-mdn.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,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,20 +1,24 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
import { HTTPError, RequestError } from 'got/dist/source';
|
||||
|
||||
export class ApiV2Error extends Error {
|
||||
override readonly name = 'ApiV2Error';
|
||||
export default class ApiV2Error extends RequestError {
|
||||
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);
|
||||
constructor(
|
||||
originalError: HTTPError,
|
||||
response: {
|
||||
message: string;
|
||||
code: string;
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
}
|
||||
) {
|
||||
super(response.message, originalError, originalError.request);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import assert from 'assert';
|
||||
import { RequestError } from 'got/dist/source';
|
||||
import nock from 'nock';
|
||||
|
||||
import { ApiV2Error } from '../ApiV2Error';
|
||||
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
|
||||
import ApiV2Error from '../ApiV2Error';
|
||||
import { apiClient, getExpoApiBaseUrl } from '../api';
|
||||
|
||||
describe(ApiV2Client, () => {
|
||||
describe('apiClient', () => {
|
||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl())
|
||||
.post('/v2/test')
|
||||
.post('/--/api/v2/test')
|
||||
.reply(400, {
|
||||
errors: [
|
||||
{
|
||||
@@ -22,9 +23,7 @@ describe(ApiV2Client, () => {
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
||||
body: {},
|
||||
});
|
||||
await apiClient.post('test');
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
@@ -40,17 +39,15 @@ describe(ApiV2Client, () => {
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
nock(getExpoApiBaseUrl()).post('/v2/test').reply(500, 'Something went wrong');
|
||||
nock(getExpoApiBaseUrl()).post('/--/api/v2/test').reply(500, 'Something went wrong');
|
||||
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
||||
body: {},
|
||||
});
|
||||
await apiClient.post('test');
|
||||
} catch (e: any) {
|
||||
error = e;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error).toBeInstanceOf(RequestError);
|
||||
expect(error).not.toBeInstanceOf(ApiV2Error);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Actor } 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;
|
||||
};
|
||||
}
|
||||
|
||||
export function createTestProject(
|
||||
user: Actor,
|
||||
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 owner = appJSONExtraData?.owner as string | undefined;
|
||||
const appJSON: AppJSON = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: owner ?? (user.__typename === 'User' ? user.username : undefined),
|
||||
...appJSONExtraData,
|
||||
},
|
||||
};
|
||||
return {
|
||||
appJSON,
|
||||
packageJSON,
|
||||
projectRoot,
|
||||
projectTree: {
|
||||
[projectRoot + '/package.json']: JSON.stringify(packageJSON, null, 2),
|
||||
[projectRoot + '/app.json']: JSON.stringify(appJSON, null, 2),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export function asMock(fn: any): jest.Mock {
|
||||
return fn as jest.Mock;
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||
import os from 'os';
|
||||
import { URL } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import UserSettings from './user/UserSettings';
|
||||
import { easCliVersion } from './utils/easCli';
|
||||
|
||||
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||
darwin: 'Mac',
|
||||
win32: 'Windows',
|
||||
linux: 'Linux',
|
||||
};
|
||||
|
||||
let rudderstackClient: RudderAnalytics | null = null;
|
||||
let userIdentified = false;
|
||||
let identifyData: {
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
traits: Record<string, any>;
|
||||
} | null = null;
|
||||
|
||||
export async function initAsync(): Promise<void> {
|
||||
// TODO: remove after some time
|
||||
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||
if (amplitudeEnabled !== null) {
|
||||
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||
}
|
||||
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||
if (amplitudeDeviceId !== null) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||
}
|
||||
// TODO: cut here
|
||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||
await UserSettings.setAsync('analyticsEnabled', false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||
if (analyticsEnabled) {
|
||||
const config =
|
||||
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',
|
||||
};
|
||||
|
||||
rudderstackClient = new RudderAnalytics(
|
||||
config.rudderstackWriteKey,
|
||||
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||
{
|
||||
flushInterval: 300,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||
const deviceId = savedDeviceId ?? uuidv4();
|
||||
if (!savedDeviceId) {
|
||||
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||
}
|
||||
|
||||
identifyData = {
|
||||
userId,
|
||||
deviceId,
|
||||
traits,
|
||||
};
|
||||
|
||||
ensureUserIdentified();
|
||||
}
|
||||
|
||||
export async function flushAsync(): Promise<void> {
|
||||
if (rudderstackClient) {
|
||||
await rudderstackClient.flush();
|
||||
}
|
||||
}
|
||||
|
||||
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||
if (!rudderstackClient) {
|
||||
return;
|
||||
}
|
||||
ensureUserIdentified();
|
||||
|
||||
const { userId, deviceId } = identifyData ?? {};
|
||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||
|
||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||
rudderstackClient.track({
|
||||
event: name,
|
||||
properties: { ...properties, ...commonEventProperties },
|
||||
...identity,
|
||||
context: getRudderStackContext(),
|
||||
});
|
||||
}
|
||||
|
||||
function ensureUserIdentified(): void {
|
||||
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||
return;
|
||||
}
|
||||
|
||||
rudderstackClient.identify({
|
||||
userId: identifyData.userId,
|
||||
anonymousId: identifyData.deviceId,
|
||||
traits: identifyData.traits,
|
||||
});
|
||||
userIdentified = true;
|
||||
}
|
||||
|
||||
function 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 },
|
||||
};
|
||||
}
|
||||
|
||||
export enum AnalyticsEvent {
|
||||
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
|
||||
}
|
||||
@@ -1,234 +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 =
|
||||
!process.env.https_proxy && // disable analytics if running behind proxy
|
||||
(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;
|
||||
}
|
||||
}
|
||||
+39
-86
@@ -1,87 +1,50 @@
|
||||
import { JSONValue } from '@expo/json-file';
|
||||
import got, { HTTPError, NormalizedOptions, RequestError } from 'got';
|
||||
|
||||
import { ApiV2Error } from './ApiV2Error';
|
||||
import fetch, { RequestError, RequestInit } from './fetch';
|
||||
import ApiV2Error from './ApiV2Error';
|
||||
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||
|
||||
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;
|
||||
}
|
||||
},
|
||||
],
|
||||
beforeError: [
|
||||
(error: RequestError): RequestError => {
|
||||
if (error instanceof HTTPError) {
|
||||
let result: { [key: string]: any };
|
||||
try {
|
||||
result = JSON.parse(error.response.body as string);
|
||||
} catch (e2) {
|
||||
return error;
|
||||
}
|
||||
if (result.errors?.length) {
|
||||
return new ApiV2Error(error, result.errors[0]);
|
||||
}
|
||||
}
|
||||
return error;
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +57,3 @@ export function getExpoWebsiteBaseUrl(): string {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,184 +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 {
|
||||
SelectPromptEntry,
|
||||
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) => SelectPromptEntry;
|
||||
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,
|
||||
makePartialChoiceObject: 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,96 +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,
|
||||
BuildResourceClass,
|
||||
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,
|
||||
resourceClass: BuildResourceClass.AndroidMedium,
|
||||
};
|
||||
}
|
||||
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,97 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig, AndroidManifest } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
const result = AndroidConfig.Updates.setUpdatesConfig(exp, androidManifest, accountName);
|
||||
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, result);
|
||||
}
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} 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);
|
||||
let androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.areVersionsSynced(exp, androidManifest)) {
|
||||
androidManifest = AndroidConfig.Updates.setVersionsConfig(exp, androidManifest);
|
||||
await AndroidConfig.Manifest.writeAndroidManifestAsync(androidManifestPath, androidManifest);
|
||||
}
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSynced(exp, androidManifest, accountName)) {
|
||||
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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in AndroidManifest.xml
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
|
||||
if (!AndroidConfig.Updates.isMainApplicationMetaDataSet(androidManifest)) {
|
||||
throw new Error('Missing values in AndroidManifest.xml');
|
||||
}
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
return AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.RELEASE_CHANNEL
|
||||
);
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const androidManifest = await getAndroidManifestAsync(projectDir);
|
||||
const stringifiedRequestHeaders = AndroidConfig.Manifest.getMainApplicationMetaDataValue(
|
||||
androidManifest,
|
||||
AndroidConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY
|
||||
);
|
||||
if (!stringifiedRequestHeaders) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(stringifiedRequestHeaders)['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getAndroidManifestAsync(projectDir: string): Promise<AndroidManifest> {
|
||||
const androidManifestPath = await AndroidConfig.Paths.getAndroidManifestAsync(projectDir);
|
||||
if (!androidManifestPath) {
|
||||
throw new Error(`Could not find AndroidManifest.xml in project directory: "${projectDir}"`);
|
||||
}
|
||||
return AndroidConfig.Manifest.readAndroidManifestAsync(androidManifestPath);
|
||||
}
|
||||
File renamed without changes.
@@ -73,39 +73,39 @@ describe(bumpVersionAsync, () => {
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/app',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/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');
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
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 () => {
|
||||
it('bumps expo.version and gradle versionCode when strategy = BumpStrategy.SHORT_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',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/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');
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
|
||||
@@ -116,17 +116,17 @@ describe(bumpVersionAsync, () => {
|
||||
|
||||
await bumpVersionAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/app',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/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');
|
||||
const buildGradle = await getAppBuildGradleAsync('/repo');
|
||||
assert(buildGradle);
|
||||
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
|
||||
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
|
||||
@@ -140,11 +140,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.VERSION_CODE,
|
||||
projectDir: '/app',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(127);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
@@ -156,11 +156,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||
projectDir: '/app',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.1');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.1');
|
||||
@@ -172,11 +172,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
||||
|
||||
await bumpVersionInAppJsonAsync({
|
||||
bumpStrategy: BumpStrategy.NOOP,
|
||||
projectDir: '/app',
|
||||
projectDir: '/repo',
|
||||
exp: fakeExp,
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/app/app.json');
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
expect(fakeExp.version).toBe('5.0.0');
|
||||
expect(fakeExp.android?.versionCode).toBe(126);
|
||||
expect(appJSON.expo.version).toBe('5.0.0');
|
||||
@@ -189,7 +189,7 @@ describe(maybeResolveVersionsAsync, () => {
|
||||
it('reads the versions from native code', async () => {
|
||||
const exp = initBareWorkflowProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/app',
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
@@ -201,7 +201,7 @@ describe(maybeResolveVersionsAsync, () => {
|
||||
it('reads the versions from expo config', async () => {
|
||||
const exp = initManagedProject();
|
||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||
'/app',
|
||||
'/repo',
|
||||
exp,
|
||||
{} as BuildProfile<Platform.ANDROID>
|
||||
);
|
||||
@@ -240,7 +240,7 @@ function initBareWorkflowProject({
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
},
|
||||
'/app'
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
@@ -261,7 +261,7 @@ function initManagedProject(): ExpoConfig {
|
||||
expo: fakeExp,
|
||||
}),
|
||||
},
|
||||
'/app'
|
||||
'/repo'
|
||||
);
|
||||
|
||||
return fakeExp;
|
||||
|
||||
@@ -1,42 +1,36 @@
|
||||
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 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 { GradleBuildContext, resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { findAccountByName } from '../../user/Account';
|
||||
import {
|
||||
BuildRequestSender,
|
||||
CredentialsResult,
|
||||
JobData,
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } from './configure';
|
||||
import { transformJob } from './graphql';
|
||||
import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
import { resolveRemoteVersionCodeAsync } from './version';
|
||||
|
||||
export async function createAndroidContextAsync(
|
||||
ctx: CommonContext<Platform.ANDROID>
|
||||
): Promise<AndroidBuildContext> {
|
||||
export async function prepareAndroidBuildAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (buildProfile.distribution === 'internal' && buildProfile.gradleCommand?.match(/bundle/)) {
|
||||
@@ -50,55 +44,35 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
|
||||
if (!confirmed) {
|
||||
Log.error('Update eas.json and come back again.');
|
||||
Log.error('Please 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);
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
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);
|
||||
return await ensureAndroidCredentialsAsync(ctx, gradleContext);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync(ctx.graphqlClient, {
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
localAutoIncrement:
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
buildProfile,
|
||||
});
|
||||
},
|
||||
getMetadataContext: () => ({
|
||||
gradleContext,
|
||||
}),
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData<AndroidCredentials>
|
||||
@@ -108,16 +82,14 @@ export async function prepareAndroidBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
|
||||
return await BuildMutation.createAndroidBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -128,7 +100,8 @@ function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean
|
||||
}
|
||||
|
||||
async function ensureAndroidCredentialsAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
gradleContext?: GradleBuildContext
|
||||
): Promise<CredentialsResult<AndroidCredentials> | undefined> {
|
||||
if (!shouldProvideCredentials(ctx)) {
|
||||
return;
|
||||
@@ -136,12 +109,12 @@ async function ensureAndroidCredentialsAsync(
|
||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
ctx.android.gradleContext
|
||||
gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
app: {
|
||||
account: nullthrows(
|
||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
||||
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||
`You do not have access to account: ${ctx.accountName}`
|
||||
),
|
||||
projectName: ctx.projectName,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile, AndroidVersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/android/applicationId';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureAndroidAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasAndroidNativeProject) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
await AndroidConfig.EasBuild.configureEasBuildAsync(ctx.projectDir);
|
||||
|
||||
const easGradlePath = AndroidConfig.EasBuild.getEasBuildGradlePath(ctx.projectDir);
|
||||
await getVcsClient().trackFileAsync(easGradlePath);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
Log.withTick('Android project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: AndroidBuildProfile;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (!(await AndroidConfig.EasBuild.isEasBuildGradleConfiguredAsync(projectDir))) {
|
||||
throw new Error(
|
||||
'Project is not configured. Please run "eas build:configure" to configure the project.'
|
||||
);
|
||||
}
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
@@ -14,14 +13,11 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
|
||||
gradleCommand: job.gradleCommand,
|
||||
applicationArchivePath: job.applicationArchivePath,
|
||||
buildArtifactPaths: job.buildArtifactPaths,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
buildType: job.buildType && transformBuildType(job.buildType),
|
||||
developmentClient: job.developmentClient,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Job,
|
||||
Platform,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
@@ -31,8 +24,8 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildProfile: AndroidBuildProfile = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const { credentials } = jobData;
|
||||
@@ -79,20 +72,9 @@ export async function prepareJobAsync(
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
artifactPath: buildProfile.artifactPath,
|
||||
buildType,
|
||||
username,
|
||||
...(ctx.android.versionCodeOverride && {
|
||||
version: {
|
||||
versionCode: ctx.android.versionCodeOverride,
|
||||
},
|
||||
}),
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
|
||||
@@ -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,22 +1,16 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig, Updates } from '@expo/config-plugins';
|
||||
import { AndroidConfig } 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';
|
||||
@@ -52,10 +46,9 @@ export async function bumpVersionAsync({
|
||||
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await updateNativeVersionsAsync({
|
||||
await writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
version: exp.version,
|
||||
versionCode: exp.android?.versionCode,
|
||||
exp,
|
||||
});
|
||||
Log.log('Synchronized versions with build gradle');
|
||||
}
|
||||
@@ -81,7 +74,7 @@ export async function bumpVersionInAppJsonAsync({
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const versionCode = AndroidConfig.Version.getVersionCode(exp);
|
||||
const bumpedVersionCode = getNextVersionCode(versionCode);
|
||||
const bumpedVersionCode = versionCode + 1;
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
|
||||
versionCode
|
||||
@@ -112,7 +105,7 @@ export async function maybeResolveVersionsAsync(
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
};
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
return {};
|
||||
}
|
||||
} else {
|
||||
@@ -123,33 +116,21 @@ export async function maybeResolveVersionsAsync(
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateNativeVersionsAsync({
|
||||
async function writeVersionsToBuildGradleAsync({
|
||||
projectDir,
|
||||
version,
|
||||
versionCode,
|
||||
exp,
|
||||
}: {
|
||||
projectDir: string;
|
||||
version?: string;
|
||||
versionCode?: number;
|
||||
}): Promise<void> {
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
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}`
|
||||
);
|
||||
}
|
||||
let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
|
||||
updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
|
||||
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
|
||||
return updatedBuildGradle;
|
||||
}
|
||||
|
||||
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
|
||||
@@ -170,101 +151,3 @@ async function writeBuildGradleAsync({
|
||||
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
|
||||
await fs.writeFile(buildGradlePath, buildGradle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns buildNumber that will be used for the next build. If current build profile
|
||||
* has an 'autoIncrement' option set, it increments the version on server.
|
||||
*/
|
||||
export async function resolveRemoteVersionCodeAsync(
|
||||
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);
|
||||
}
|
||||
}
|
||||
+203
-497
@@ -1,44 +1,24 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import cliProgress from 'cli-progress';
|
||||
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,
|
||||
BuildResourceClass,
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildFragment, 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 { confirmAsync } from '../prompts';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { requestedPlatformDisplayNames } from '../platform';
|
||||
import { uploadAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { transformMetadata } from './graphql';
|
||||
import { LocalBuildMode, runLocalBuildAsync } from './local';
|
||||
import { collectMetadataAsync } from './metadata';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
import { MetadataContext, collectMetadataAsync } from './metadata';
|
||||
import { TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
|
||||
@@ -58,24 +38,14 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||
ensureCredentialsAsync(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
ensureProjectConfiguredAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||
getMetadataContext: () => MetadataContext<TPlatform>;
|
||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||
sendBuildRequestAsync(
|
||||
appId: string,
|
||||
job: TJob,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult>;
|
||||
sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
|
||||
}
|
||||
|
||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||
|
||||
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
|
||||
return {
|
||||
resourceClass: ctx.resourceClass,
|
||||
};
|
||||
}
|
||||
|
||||
export async function prepareBuildRequestForPlatformAsync<
|
||||
TPlatform extends Platform,
|
||||
Credentials,
|
||||
@@ -83,26 +53,20 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||
const { ctx } = builder;
|
||||
const credentialsResult = await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => await builder.ensureCredentialsAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
}
|
||||
);
|
||||
|
||||
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,
|
||||
successEvent: Event.GATHER_CREDENTIALS_SUCCESS,
|
||||
failureEvent: Event.GATHER_CREDENTIALS_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
if (!ctx.skipProjectConfiguration) {
|
||||
await withAnalyticsAsync(async () => await builder.ensureProjectConfiguredAsync(ctx), {
|
||||
successEvent: Event.CONFIGURE_PROJECT_SUCCESS,
|
||||
failureEvent: Event.CONFIGURE_PROJECT_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
});
|
||||
}
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
@@ -112,124 +76,80 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
let projectArchive: ArchiveSource | undefined;
|
||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
};
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: process.cwd(),
|
||||
};
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
};
|
||||
}
|
||||
assert(projectArchive);
|
||||
const projectArchive = ctx.local
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.S3,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const buildParams = resolveBuildParamsInput(ctx);
|
||||
const metadataContext = builder.getMetadataContext();
|
||||
const metadata = await collectMetadataAsync(ctx, metadataContext);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
credentials: credentialsResult?.credentials,
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||
if (ctx.local) {
|
||||
await runLocalBuildAsync(job);
|
||||
return undefined;
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
|
||||
buildId: nullthrows(process.env.EAS_BUILD_ID),
|
||||
metadata: transformMetadata(metadata),
|
||||
});
|
||||
printJsonOnlyOutput({ job, metadata });
|
||||
return undefined;
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown localBuildMode.');
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata);
|
||||
} catch (error: any) {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed, please 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, please try again later. Check 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, please try again later. Check https://status.expo.dev/ for updates.'
|
||||
);
|
||||
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[job.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, please report the issue.'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const SERVER_SIDE_DEFINED_ERRORS = [
|
||||
'TURTLE_DEPRECATED_JOB_FORMAT',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED_IOS',
|
||||
'EAS_BUILD_FREE_TIER_DISABLED_ANDROID',
|
||||
'VALIDATION_ERROR',
|
||||
];
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
|
||||
|
||||
if (SERVER_SIDE_DEFINED_ERRORS.includes(error?.graphQLErrors?.[0]?.extensions?.errorCode)) {
|
||||
Log.error(error?.graphQLErrors?.[0]?.message);
|
||||
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_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;
|
||||
}
|
||||
|
||||
async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
ctx: BuildContext<TPlatform>
|
||||
): Promise<string> {
|
||||
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,
|
||||
const { bucketKey } = await uploadAsync(
|
||||
UploadSessionType.EasBuildProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
@@ -237,16 +157,18 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
completedMessage: (duration: string) =>
|
||||
`Uploaded to EAS ${chalk.dim(duration)} ${learnMore(
|
||||
'https://expo.fyi/eas-build-archive'
|
||||
)}`,
|
||||
})
|
||||
);
|
||||
return bucketKey;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
successEvent: Event.PROJECT_UPLOAD_SUCCESS,
|
||||
failureEvent: Event.PROJECT_UPLOAD_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
} finally {
|
||||
@@ -259,12 +181,10 @@ 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
|
||||
metadata: Metadata
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
return await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
if (Log.isDebug) {
|
||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||
@@ -273,353 +193,139 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
metadata,
|
||||
buildParams
|
||||
metadata
|
||||
);
|
||||
|
||||
printDeprecationWarnings(deprecationInfo);
|
||||
return build;
|
||||
},
|
||||
{
|
||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||
properties: ctx.analyticsEventProperties,
|
||||
successEvent: Event.BUILD_REQUEST_SUCCESS,
|
||||
failureEvent: Event.BUILD_REQUEST_FAIL,
|
||||
trackingCtx: ctx.trackingCtx,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export type MaybeBuildFragment = BuildFragment | null;
|
||||
async function withAnalyticsAsync<Result>(
|
||||
fn: () => Promise<Result>,
|
||||
analytics: {
|
||||
successEvent: Event;
|
||||
failureEvent: Event;
|
||||
trackingCtx: TrackingContext;
|
||||
}
|
||||
): Promise<Result> {
|
||||
try {
|
||||
const result = await fn();
|
||||
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
Analytics.logEvent(analytics.failureEvent, {
|
||||
...analytics.trackingCtx,
|
||||
reason: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
buildIds,
|
||||
accountName,
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
|
||||
{ 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();
|
||||
}
|
||||
while (true) {
|
||||
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
? await handleSingleBuildProgressAsync(
|
||||
{ build: builds[0], accountName, projectDir, nonInteractive },
|
||||
{ spinner }
|
||||
)
|
||||
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
|
||||
if (!refetch) {
|
||||
return builds;
|
||||
buildIds: string[],
|
||||
{ timeoutSec = 3600, intervalSec = 30 } = {}
|
||||
): Promise<(BuildFragment | null)[]> {
|
||||
Log.log(
|
||||
`Waiting for build${buildIds.length > 1 ? 's' : ''} 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: (BuildFragment | null)[] = await Promise.all(
|
||||
buildIds.map(async buildId => {
|
||||
try {
|
||||
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||
} catch (err) {
|
||||
Log.debug('Failed to fetch the build status', err);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
if (build !== null) {
|
||||
switch (build.status) {
|
||||
case BuildStatus.Finished:
|
||||
spinner.succeed('Build finished');
|
||||
return builds;
|
||||
case BuildStatus.New:
|
||||
spinner.text = 'Build created';
|
||||
break;
|
||||
case BuildStatus.InQueue:
|
||||
spinner.text = 'Build queued...';
|
||||
break;
|
||||
case BuildStatus.Canceled:
|
||||
spinner.text = 'Build canceled';
|
||||
spinner.stopAndPersist();
|
||||
return builds;
|
||||
case BuildStatus.InProgress:
|
||||
spinner.text = 'Build in progress...';
|
||||
break;
|
||||
case BuildStatus.Errored:
|
||||
spinner.fail('Build failed');
|
||||
if (build.error) {
|
||||
return builds;
|
||||
} else {
|
||||
throw new Error(`Standalone build failed!`);
|
||||
}
|
||||
default:
|
||||
spinner.warn('Unknown status');
|
||||
throw new Error(`Unknown build status: ${build.status} - aborting!`);
|
||||
}
|
||||
} else {
|
||||
if (!spinner.text) {
|
||||
spinner.text = 'Could not fetch the build status. Check your network connection.';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (builds.filter(build => build?.status === BuildStatus.Finished).length === builds.length) {
|
||||
spinner.succeed('All builds have finished');
|
||||
return builds;
|
||||
} else if (
|
||||
builds.filter(build =>
|
||||
build?.status
|
||||
? [BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(
|
||||
build.status
|
||||
)
|
||||
: false
|
||||
).length === builds.length
|
||||
) {
|
||||
spinner.fail('Some of the builds were canceled or failed.');
|
||||
return builds;
|
||||
} else {
|
||||
const newBuilds = builds.filter(build => build?.status === BuildStatus.New).length;
|
||||
const inQueue = builds.filter(build => build?.status === BuildStatus.InQueue).length;
|
||||
const inProgress = builds.filter(build => build?.status === BuildStatus.InProgress).length;
|
||||
const errored = builds.filter(build => build?.status === BuildStatus.Errored).length;
|
||||
const finished = builds.filter(build => build?.status === BuildStatus.Finished).length;
|
||||
const canceled = builds.filter(build => build?.status === BuildStatus.Canceled).length;
|
||||
const unknown = builds.length - newBuilds - inQueue - inProgress - errored - finished;
|
||||
spinner.text = [
|
||||
newBuilds && `Builds created: ${newBuilds}`,
|
||||
inQueue && `Builds in queue: ${inQueue}`,
|
||||
inProgress && `Builds in progress: ${inProgress}`,
|
||||
canceled && `Builds canceled: ${canceled}`,
|
||||
errored && chalk.red(`Builds failed: ${errored}`),
|
||||
finished && chalk.green(`Builds finished: ${finished}`),
|
||||
unknown && chalk.red(`Builds with unknown status: ${unknown}`),
|
||||
]
|
||||
.filter(i => i)
|
||||
.join('\t');
|
||||
}
|
||||
}
|
||||
time = new Date().getTime();
|
||||
await sleepAsync(intervalSec * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: MaybeBuildFragment;
|
||||
accountName: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
},
|
||||
{ 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 ${link(
|
||||
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' &&
|
||||
typeof build.estimatedWaitTimeLeftSeconds === '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 ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
build.platform === AppPlatform.Ios &&
|
||||
[BuildResourceClass.IosIntelLarge, BuildResourceClass.IosIntelMedium].includes(
|
||||
build.resourceClass
|
||||
)
|
||||
) {
|
||||
let askToSwitchToM1 = false;
|
||||
if (
|
||||
build.priority === BuildPriority.High &&
|
||||
build.estimatedWaitTimeLeftSeconds >= /* 10 minutes */ 10 * 60
|
||||
) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`Warning: Priority queue wait time for legacy iOS Intel workers is longer than usual (more than 10 minutes).`
|
||||
);
|
||||
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
|
||||
Log.warn(
|
||||
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
|
||||
learnMoreMessage: 'Learn more on switching to M1 workers.',
|
||||
})
|
||||
);
|
||||
askToSwitchToM1 = true;
|
||||
} else if (
|
||||
build.priority !== BuildPriority.High &&
|
||||
build.estimatedWaitTimeLeftSeconds >= /* 120 minutes */ 120 * 60
|
||||
) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`Warning: Free tier queue wait time for legacy iOS Intel workers is longer than usual.`
|
||||
);
|
||||
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
|
||||
Log.warn(
|
||||
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
|
||||
learnMoreMessage: 'Learn more on switching to M1 workers.',
|
||||
})
|
||||
);
|
||||
askToSwitchToM1 = true;
|
||||
}
|
||||
if (!nonInteractive && askToSwitchToM1) {
|
||||
const shouldSwitchToM1 = await confirmAsync({
|
||||
message: `Switch iOS builds to M1 workers (modifies build profiles in eas.json)?`,
|
||||
});
|
||||
if (shouldSwitchToM1) {
|
||||
await updateIosBuildProfilesToUseM1WorkersAsync(projectDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]: 'Free tier queue',
|
||||
[BuildPriority.NormalPlus]: 'Free tier 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();
|
||||
}
|
||||
|
||||
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
for (const profileName of profileNames) {
|
||||
easJsonRawObject.build[profileName].ios = {
|
||||
...easJsonRawObject.build[profileName].ios,
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
};
|
||||
}
|
||||
return easJsonRawObject;
|
||||
});
|
||||
await easJsonAccessor.writeAsync();
|
||||
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
|
||||
spinner.warn('Timed out');
|
||||
throw new Error(
|
||||
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,98 @@
|
||||
import { getConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonAccessor, ResourceClass } from '@expo/eas-json';
|
||||
import { EasJson, EasJsonReader } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { confirmAsync, promptAsync } from '../prompts';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
import { configureAndroidAsync } from './android/configure';
|
||||
import { ConfigureContext } from './context';
|
||||
import { configureIosAsync } from './ios/configure';
|
||||
import { commitPromptAsync, maybeBailOnRepoStatusAsync } from './utils/repository';
|
||||
|
||||
interface ConfigureParams {
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
const configureCommitMessage = {
|
||||
[RequestedPlatform.Android]: 'Configure EAS Build for Android',
|
||||
[RequestedPlatform.Ios]: 'Configure EAS Build for iOS',
|
||||
[RequestedPlatform.All]: 'Configure EAS Build',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
projectDir: string,
|
||||
requestedPlatform: RequestedPlatform
|
||||
): Promise<void> {
|
||||
if (await fs.pathExists(EasJsonReader.formatEasJsonPath(projectDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await configureAsync(configureParams);
|
||||
return true;
|
||||
const message = 'This project is not configured to build with EAS. Set it up now?';
|
||||
const confirm = await confirmAsync({ message });
|
||||
if (confirm) {
|
||||
await configureAsync({
|
||||
projectDir,
|
||||
platform: requestedPlatform,
|
||||
});
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
error(
|
||||
'Build process requires clean working tree, please commit all your changes and run `eas build` again',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
} else {
|
||||
error(
|
||||
`Aborting, please run ${chalk.bold('eas build:configure')} or create eas.json (${learnMore(
|
||||
'https://docs.expo.dev/build/eas-json'
|
||||
)})`,
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||
export async function configureAsync(options: {
|
||||
platform: RequestedPlatform;
|
||||
projectDir: string;
|
||||
}): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await maybeBailOnRepoStatusAsync();
|
||||
|
||||
await createEasJsonAsync(projectDir);
|
||||
const { exp } = getConfig(options.projectDir, { skipSDKVersionRequirement: true });
|
||||
|
||||
const ctx: ConfigureContext = {
|
||||
user: await ensureLoggedInAsync(),
|
||||
projectDir: options.projectDir,
|
||||
exp,
|
||||
requestedPlatform: options.platform,
|
||||
shouldConfigureAndroid: [RequestedPlatform.All, RequestedPlatform.Android].includes(
|
||||
options.platform
|
||||
),
|
||||
shouldConfigureIos: [RequestedPlatform.All, RequestedPlatform.Ios].includes(options.platform),
|
||||
hasAndroidNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.ANDROID)) === Workflow.GENERIC,
|
||||
hasIosNativeProject:
|
||||
(await resolveWorkflowAsync(options.projectDir, Platform.IOS)) === Workflow.GENERIC,
|
||||
};
|
||||
|
||||
Log.newLine();
|
||||
await ensureEasJsonExistsAsync(ctx);
|
||||
if (ctx.shouldConfigureAndroid) {
|
||||
await configureAndroidAsync(ctx);
|
||||
}
|
||||
if (ctx.shouldConfigureIos) {
|
||||
await configureIosAsync(ctx);
|
||||
}
|
||||
|
||||
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||
Log.newLine();
|
||||
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||
nonInteractive,
|
||||
});
|
||||
await reviewAndCommitChangesAsync(configureCommitMessage[options.platform]);
|
||||
} else if (!(await getVcsClient().hasUncommittedChangesAsync())) {
|
||||
Log.newLine();
|
||||
Log.withTick('No changes were necessary, the project is already configured correctly.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,21 +104,11 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
@@ -86,39 +127,70 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M1_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
export async function ensureEasJsonExistsAsync(ctx: ConfigureContext): Promise<void> {
|
||||
const easJsonPath = EasJsonReader.formatEasJsonPath(ctx.projectDir);
|
||||
|
||||
if (await fs.pathExists(easJsonPath)) {
|
||||
const reader = new EasJsonReader(ctx.projectDir);
|
||||
await reader.readAndValidateAsync();
|
||||
|
||||
Log.withTick('Validated eas.json');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAndroidNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||
const hasIosNativeProject =
|
||||
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||
const easJson =
|
||||
hasAndroidNativeProject || hasIosNativeProject
|
||||
ctx.hasAndroidNativeProject && ctx.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')}`);
|
||||
Log.withTick('Generated eas.json');
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
Skip,
|
||||
}
|
||||
|
||||
async function reviewAndCommitChangesAsync(
|
||||
initialCommitMessage: string,
|
||||
askedFirstTime: boolean = true
|
||||
): Promise<void> {
|
||||
const { selected } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selected',
|
||||
message: 'Can we commit these changes to git for you?',
|
||||
choices: [
|
||||
{ title: 'Yes', value: ShouldCommitChanges.Yes },
|
||||
...(askedFirstTime
|
||||
? [{ title: 'Show the diff and ask me again', value: ShouldCommitChanges.ShowDiffFirst }]
|
||||
: []),
|
||||
{
|
||||
title: 'Skip committing changes, I will do it later on my own',
|
||||
value: ShouldCommitChanges.Skip,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (selected === ShouldCommitChanges.Yes) {
|
||||
await commitPromptAsync({ initialCommitMessage });
|
||||
Log.withTick('Committed changes');
|
||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||
await getVcsClient().showDiffAsync();
|
||||
await reviewAndCommitChangesAsync(initialCommitMessage, false);
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,155 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile, BuildProfile, IosBuildProfile } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
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 { RequestedPlatform } from '../platform';
|
||||
import { getExpoConfig } from '../project/expoConfig';
|
||||
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { findAccountByName } from '../user/Account';
|
||||
import { Actor } from '../user/User';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { TrackingContext } from './types';
|
||||
import Analytics, { Event } from './utils/analytics';
|
||||
|
||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||
|
||||
export interface AndroidBuildContext {
|
||||
applicationId: string;
|
||||
gradleContext?: GradleBuildContext;
|
||||
versionCodeOverride?: string;
|
||||
}
|
||||
|
||||
export interface IosBuildContext {
|
||||
bundleIdentifier: string;
|
||||
applicationTarget: Target;
|
||||
targets: Target[];
|
||||
xcodeBuildContext: XcodeBuildContext;
|
||||
buildNumberOverride?: string;
|
||||
export interface ConfigureContext {
|
||||
user: Actor;
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
requestedPlatform: RequestedPlatform;
|
||||
shouldConfigureAndroid: boolean;
|
||||
shouldConfigureIos: boolean;
|
||||
hasAndroidNativeProject: boolean;
|
||||
hasIosNativeProject: boolean;
|
||||
}
|
||||
|
||||
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;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
noWait: boolean;
|
||||
runFromCI: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
projectName: string;
|
||||
message?: string;
|
||||
analyticsEventProperties: AnalyticsEventProperties;
|
||||
skipProjectConfiguration: boolean;
|
||||
trackingCtx: TrackingContext;
|
||||
user: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
workflow: Workflow;
|
||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||
}
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
clearCache = false,
|
||||
local,
|
||||
nonInteractive = false,
|
||||
platform,
|
||||
projectDir,
|
||||
skipProjectConfiguration = false,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
clearCache: boolean;
|
||||
local: boolean;
|
||||
nonInteractive: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
skipProjectConfiguration: boolean;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||
|
||||
const user = await ensureLoggedInAsync();
|
||||
const accountName = getProjectAccountName(exp, user);
|
||||
const projectName = exp.slug;
|
||||
const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
const accountId = findAccountByName(user.accounts, accountName)?.id;
|
||||
|
||||
const credentialsCtx = new CredentialsContext({
|
||||
exp,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
user,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
});
|
||||
const trackingCtx = {
|
||||
tracking_id: uuidv4(),
|
||||
platform,
|
||||
...(accountId && { account_id: accountId }),
|
||||
account_name: accountName,
|
||||
project_id: projectId,
|
||||
project_type: workflow,
|
||||
...devClientProperties,
|
||||
};
|
||||
Analytics.logEvent(Event.BUILD_COMMAND, trackingCtx);
|
||||
|
||||
return {
|
||||
accountName,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
clearCache,
|
||||
credentialsCtx,
|
||||
exp,
|
||||
local,
|
||||
nonInteractive,
|
||||
platform,
|
||||
projectDir,
|
||||
projectId,
|
||||
projectName,
|
||||
skipProjectConfiguration,
|
||||
trackingCtx,
|
||||
user,
|
||||
workflow,
|
||||
};
|
||||
}
|
||||
|
||||
function getDevClientEventProperties({
|
||||
platform,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
}: {
|
||||
platform: Platform;
|
||||
projectDir: string;
|
||||
buildProfile: AndroidBuildProfile | IosBuildProfile;
|
||||
}): Partial<TrackingContext> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson, ResourceClass } 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 { 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';
|
||||
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
buildProfile,
|
||||
easJsonCliConfig,
|
||||
clearCache = false,
|
||||
localBuildOptions,
|
||||
nonInteractive,
|
||||
noWait,
|
||||
platform,
|
||||
projectDir,
|
||||
resourceClassFlag,
|
||||
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;
|
||||
resourceClassFlag?: ResourceClass;
|
||||
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 resourceClass = await resolveBuildResourceClassAsync(
|
||||
buildProfile,
|
||||
platform,
|
||||
projectDir,
|
||||
exp,
|
||||
resourceClassFlag
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,11 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
BuildTrigger,
|
||||
Metadata,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildMode,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
BuildTrigger as GraphQLBuildTrigger,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
@@ -24,11 +16,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
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,
|
||||
@@ -42,7 +29,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
buildMode: metadata.buildMode && transformBuildMode(metadata.buildMode),
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
@@ -90,20 +76,3 @@ export function transformIosEnterpriseProvisioning(
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformBuildMode(buildMode: Metadata['buildMode']): BuildMode {
|
||||
if (buildMode === 'build') {
|
||||
return BuildMode.Build;
|
||||
} else {
|
||||
return BuildMode.Resign;
|
||||
}
|
||||
}
|
||||
|
||||
export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildTrigger {
|
||||
if (buildTrigger === BuildTrigger.EAS_CLI) {
|
||||
return GraphQLBuildTrigger.EasCli;
|
||||
} else if (buildTrigger === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
return GraphQLBuildTrigger.GitBasedIntegration;
|
||||
}
|
||||
throw new Error('Unknown build trigger');
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
|
||||
import Log from '../../log';
|
||||
import { getProjectAccountName } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { ensureValidVersions } from '../utils/updates';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export async function configureUpdatesAsync(projectDir: string, exp: ExpoConfig): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
|
||||
let expoPlist = await readExpoPlistAsync(projectDir);
|
||||
if (!IOSConfig.Updates.isPlistConfigurationSynced(exp, expoPlist, accountName)) {
|
||||
expoPlist = IOSConfig.Updates.setUpdatesConfig(exp, expoPlist, accountName);
|
||||
await writeExpoPlistAsync(projectDir, expoPlist);
|
||||
}
|
||||
// TODO: ensure ExpoPlist in pbxproj
|
||||
}
|
||||
|
||||
export async function syncUpdatesConfigurationAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig
|
||||
): Promise<void> {
|
||||
ensureValidVersions(exp);
|
||||
const accountName = getProjectAccountName(exp, await ensureLoggedInAsync());
|
||||
try {
|
||||
await ensureUpdatesConfiguredAsync(projectDir);
|
||||
} 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, accountName)) {
|
||||
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'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we assume here that Expo modules are properly configured in the project. Aside from that,
|
||||
// all that is needed on Expo SDK 43+ to configure expo-updates configuration in Expo.plist
|
||||
async function ensureUpdatesConfiguredAsync(projectDir: string): Promise<void> {
|
||||
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);
|
||||
return (await readPlistAsync(expoPlistPath)) as IOSConfig.ExpoPlist;
|
||||
}
|
||||
|
||||
async function writeExpoPlistAsync(
|
||||
projectDir: string,
|
||||
expoPlist: IOSConfig.ExpoPlist
|
||||
): Promise<void> {
|
||||
const expoPlistPath = IOSConfig.Paths.getExpoPlistPath(projectDir);
|
||||
await writePlistAsync(expoPlistPath, expoPlist);
|
||||
await getVcsClient().trackFileAsync(expoPlistPath);
|
||||
}
|
||||
|
||||
export async function readReleaseChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist = await readExpoPlistAsync(projectDir);
|
||||
return expoPlist[IOSConfig.Updates.Config.RELEASE_CHANNEL] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function readChannelSafelyAsync(projectDir: string): Promise<string | null> {
|
||||
try {
|
||||
const expoPlist: any = await readExpoPlistAsync(projectDir); // TODO-JJ remove any once IOSConfig.ExpoPlist is updated to include `EXUpdatesRequestHeaders : Record<string,string>`
|
||||
const updatesRequestHeaders = expoPlist['EXUpdatesRequestHeaders'] ?? {}; //TODO-JJ 'EXUpdatesRequestHeaders' IosConfig.Updates.Config.UPDATES_CONFIGURATION_REQUEST_HEADERS_KEY once https://github.com/expo/expo-cli/pull/3571 is published
|
||||
return updatesRequestHeaders['expo-channel-name'] ?? null;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -3,9 +3,8 @@ 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 { readPlistAsync } from '../plist';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
@@ -18,12 +17,6 @@ import {
|
||||
|
||||
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
|
||||
@@ -58,34 +51,14 @@ describe(evaluateTemplateString, () => {
|
||||
|
||||
// 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: {},
|
||||
},
|
||||
],
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -103,25 +76,13 @@ describe(bumpVersionAsync, () => {
|
||||
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: {},
|
||||
},
|
||||
],
|
||||
buildSettings: {
|
||||
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
||||
},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -136,27 +97,14 @@ describe(bumpVersionAsync, () => {
|
||||
expect(infoPlist['CFBundleVersion']).toBe('2');
|
||||
});
|
||||
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.APP_VERSION', async () => {
|
||||
it('bumps expo.version and CFBundleShortVersionString when strategy = BumpStrategy.SHORT_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: {},
|
||||
},
|
||||
],
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -173,25 +121,12 @@ describe(bumpVersionAsync, () => {
|
||||
|
||||
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: {},
|
||||
},
|
||||
],
|
||||
buildSettings: {},
|
||||
});
|
||||
|
||||
const appJSON = await fs.readJSON('/repo/app.json');
|
||||
@@ -353,9 +288,9 @@ describe(getInfoPlistPath, () => {
|
||||
|
||||
function initBareWorkflowProject({
|
||||
appVersion = '1.0.0',
|
||||
buildNumber = '1',
|
||||
version = '1',
|
||||
infoPlistName = 'Info.plist',
|
||||
}: { appVersion?: string; buildNumber?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
}: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
slug: 'myproject',
|
||||
@@ -376,7 +311,7 @@ function initBareWorkflowProject({
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${appVersion}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${buildNumber}</string>
|
||||
<string>${version}</string>
|
||||
</dict>
|
||||
</plist>`,
|
||||
},
|
||||
|
||||
@@ -1,38 +1,39 @@
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
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 { BuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
validatePNGsForManagedProjectAsync,
|
||||
} from '../validate';
|
||||
import { IosMetadataContext } from '../metadata';
|
||||
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||
import { validateAndSyncProjectConfigurationAsync } 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> {
|
||||
interface BuildConfiguration {
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}
|
||||
|
||||
export async function prepareIosBuildAsync(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): Promise<BuildRequestSender> {
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
await checkGoogleServicesFileAsync(ctx);
|
||||
await validatePNGsForManagedProjectAsync(ctx);
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
{
|
||||
@@ -42,75 +43,70 @@ export async function createIosContextAsync(
|
||||
},
|
||||
buildProfile
|
||||
);
|
||||
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,
|
||||
};
|
||||
}
|
||||
const targets = await resolveTargetsAsync(
|
||||
{
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
},
|
||||
xcodeBuildContext
|
||||
);
|
||||
const buildConfiguration = resolveBuildConfiguration(ctx, targets);
|
||||
|
||||
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);
|
||||
return ensureIosCredentialsAsync(ctx, targets);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync(ctx.graphqlClient, {
|
||||
ensureProjectConfiguredAsync: async () => {
|
||||
await validateAndSyncProjectConfigurationAsync({
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
targets: ctx.ios.targets,
|
||||
localAutoIncrement:
|
||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
projectId: ctx.projectId,
|
||||
buildProfile,
|
||||
buildSettings: buildConfiguration.buildSettings,
|
||||
});
|
||||
},
|
||||
getMetadataContext: (): IosMetadataContext => {
|
||||
return buildConfiguration;
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData<IosCredentials>
|
||||
): Promise<Job> => {
|
||||
return await prepareJobAsync(ctx, {
|
||||
...jobData,
|
||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||
});
|
||||
return await prepareJobAsync(ctx, { ...jobData, buildScheme: xcodeBuildContext.buildScheme });
|
||||
},
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
metadata: Metadata
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
|
||||
return await BuildMutation.createIosBuildAsync({
|
||||
appId,
|
||||
job: graphqlJob,
|
||||
metadata: graphqlMetadata,
|
||||
buildParams,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function resolveBuildConfiguration(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
targets: Target[]
|
||||
): BuildConfiguration {
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
return { buildSettings: {} };
|
||||
}
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
});
|
||||
const buildSettings = xcBuildConfiguration?.buildSettings ?? {};
|
||||
return {
|
||||
buildSettings,
|
||||
targetName: applicationTarget.targetName,
|
||||
buildConfiguration: applicationTarget.buildConfiguration,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosBuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject,
|
||||
} from '../../project/ios/bundleIdentifier';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { ConfigureContext } from '../context';
|
||||
import { isExpoUpdatesInstalled } from '../utils/updates';
|
||||
import { configureUpdatesAsync, syncUpdatesConfigurationAsync } from './UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function configureIosAsync(ctx: ConfigureContext): Promise<void> {
|
||||
if (!ctx.hasIosNativeProject) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||
return;
|
||||
}
|
||||
|
||||
warnIfBundleIdentifierDefinedInAppConfigForBareWorkflowProject(ctx.projectDir, ctx.exp);
|
||||
|
||||
if (isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
await configureUpdatesAsync(ctx.projectDir, ctx.exp);
|
||||
}
|
||||
Log.withTick('iOS project configured');
|
||||
}
|
||||
|
||||
export async function validateAndSyncProjectConfigurationAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
buildProfile,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: IosBuildProfile;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const { autoIncrement } = buildProfile;
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'buildNumber') {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { CredentialsContext } from '../../credentials/context';
|
||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||
import { resolveEntitlementsJsonAsync } from '../../credentials/ios/appstore/entitlements';
|
||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||
import { CredentialsResult } from '../build';
|
||||
import { BuildContext } from '../context';
|
||||
@@ -18,8 +17,11 @@ export async function ensureIosCredentialsAsync(
|
||||
}
|
||||
|
||||
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
|
||||
app: await getAppFromContextAsync(buildCtx.credentialsCtx),
|
||||
app: getAppFromContext(buildCtx.credentialsCtx),
|
||||
targets,
|
||||
iosCapabilitiesOptions: {
|
||||
entitlements: await resolveEntitlementsJsonAsync(buildCtx.projectDir, buildCtx.workflow),
|
||||
},
|
||||
distribution: buildCtx.buildProfile.distribution ?? 'store',
|
||||
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
@@ -33,27 +35,6 @@ export async function ensureIosCredentialsAsync(
|
||||
};
|
||||
}
|
||||
|
||||
export async function ensureIosCredentialsForBuildResignAsync(
|
||||
credentialsCtx: CredentialsContext,
|
||||
targets: Target[],
|
||||
buildProfile: BuildProfile<Platform.IOS>
|
||||
): Promise<CredentialsResult<IosCredentials>> {
|
||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
||||
app: await getAppFromContextAsync(credentialsCtx),
|
||||
targets,
|
||||
distribution: 'internal',
|
||||
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
|
||||
});
|
||||
|
||||
const { credentialsSource } = buildProfile;
|
||||
|
||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
||||
return {
|
||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||
return !buildCtx.buildProfile.simulator;
|
||||
}
|
||||
@@ -1,34 +1,31 @@
|
||||
import { Ios } from '@expo/eas-build-job';
|
||||
import { Env, Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: nullthrows(job.projectRootDirectory),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: transformIosSecrets(job.secrets),
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
cache: job.cache,
|
||||
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
|
||||
scheme: job.scheme,
|
||||
buildConfiguration: job.buildConfiguration,
|
||||
applicationArchivePath: job.applicationArchivePath,
|
||||
buildArtifactPaths: job.buildArtifactPaths,
|
||||
artifactPath: job.artifactPath,
|
||||
username: job.username,
|
||||
developmentClient: job.developmentClient,
|
||||
simulator: job.simulator,
|
||||
experimental: job.experimental,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformIosSecrets(secrets: {
|
||||
function transformIosSecrets(secrets: {
|
||||
buildCredentials?: Ios.BuildCredentials;
|
||||
env?: Env;
|
||||
}): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
|
||||
@@ -44,5 +41,6 @@ export function transformIosSecrets(secrets: {
|
||||
|
||||
return {
|
||||
buildCredentials,
|
||||
environmentSecrets: secrets.env,
|
||||
};
|
||||
}
|
||||
@@ -3,17 +3,12 @@ import plist from '@expo/plist';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function readPlistAsync(plistPath: string): Promise<object | null> {
|
||||
export async function readPlistAsync(plistPath: string): Promise<object> {
|
||||
if (await fs.pathExists(plistPath)) {
|
||||
const expoPlistContent = await fs.readFile(plistPath, 'utf8');
|
||||
try {
|
||||
return plist.parse(expoPlistContent);
|
||||
} catch (err: any) {
|
||||
err.message = `Failed to parse ${plistPath}. ${err.message}`;
|
||||
throw err;
|
||||
}
|
||||
return plist.parse(expoPlistContent);
|
||||
} else {
|
||||
return null;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import {
|
||||
ArchiveSource,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Ios,
|
||||
Job,
|
||||
Platform,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import path from 'path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
@@ -35,7 +26,7 @@ export async function prepareJobAsync(
|
||||
): Promise<Job> {
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const username = getUsername(ctx.exp, await ensureLoggedInAsync());
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
@@ -73,40 +64,12 @@ export async function prepareJobAsync(
|
||||
simulator: ctx.buildProfile.simulator,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
applicationArchivePath:
|
||||
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
|
||||
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
|
||||
artifactPath: ctx.buildProfile.artifactPath,
|
||||
username,
|
||||
...(ctx.ios.buildNumberOverride && {
|
||||
version: {
|
||||
buildNumber: ctx.ios.buildNumberOverride,
|
||||
},
|
||||
}),
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
|
||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||
for (const targetName of Object.keys(credentials ?? {})) {
|
||||
buildCredentials.push({
|
||||
targetName,
|
||||
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
|
||||
distributionCertificate: {
|
||||
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
|
||||
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { buildCredentials };
|
||||
}
|
||||
|
||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||
return {
|
||||
provisioningProfileBase64: targetCredentials.provisioningProfile,
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Target } from '../../credentials/ios/types';
|
||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
|
||||
export async function syncProjectConfigurationAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectDir,
|
||||
exp,
|
||||
targets,
|
||||
localAutoIncrement,
|
||||
projectId,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
targets: Target[];
|
||||
localAutoIncrement?: IosVersionAutoIncrement;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
|
||||
} else {
|
||||
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVersionBumpStrategy(autoIncrement: IosVersionAutoIncrement): BumpStrategy {
|
||||
if (autoIncrement === true) {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else if (autoIncrement === false) {
|
||||
return BumpStrategy.NOOP;
|
||||
} else if (autoIncrement === 'buildNumber') {
|
||||
return BumpStrategy.BUILD_NUMBER;
|
||||
} else {
|
||||
return BumpStrategy.APP_VERSION;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,16 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig, Updates } from '@expo/config-plugins';
|
||||
import { ExpoConfig, getConfigFilePaths } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import path from 'path';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Target } from '../../credentials/ios/types';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||
import { bumpAppVersionAsync } from '../utils/version';
|
||||
import { readPlistAsync, writePlistAsync } from './plist';
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
@@ -32,25 +22,21 @@ export async function bumpVersionAsync({
|
||||
bumpStrategy,
|
||||
projectDir,
|
||||
exp,
|
||||
targets,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
bumpStrategy: BumpStrategy;
|
||||
targets: Target[];
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
if (bumpStrategy === BumpStrategy.NOOP) {
|
||||
return;
|
||||
}
|
||||
ensureStaticConfigExists(projectDir);
|
||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
|
||||
Log.log('Updated versions in app.json');
|
||||
await updateNativeVersionsAsync({
|
||||
projectDir,
|
||||
version: exp.version,
|
||||
buildNumber: exp.ios?.buildNumber,
|
||||
targets,
|
||||
});
|
||||
await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
|
||||
Log.log('Synchronized versions with Info.plist');
|
||||
}
|
||||
|
||||
@@ -73,8 +59,10 @@ export async function bumpVersionInAppJsonAsync({
|
||||
await bumpAppVersionAsync({ appVersion, projectDir, exp });
|
||||
} else {
|
||||
const buildNumber = IOSConfig.Version.getBuildNumber(exp);
|
||||
if (isValidBuildNumber(buildNumber)) {
|
||||
const bumpedBuildNumber = getNextBuildNumber(buildNumber);
|
||||
if (buildNumber.match(/^\d+(\.\d+)*$/)) {
|
||||
const comps = buildNumber.split('.');
|
||||
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
|
||||
const bumpedBuildNumber = comps.join('.');
|
||||
Log.log(
|
||||
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
|
||||
buildNumber
|
||||
@@ -133,31 +121,35 @@ export async function readBuildNumberAsync(
|
||||
export async function maybeResolveVersionsAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
targets: Target[]
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
try {
|
||||
return {
|
||||
appBuildVersion: await readBuildNumberAsync(
|
||||
projectDir,
|
||||
exp,
|
||||
applicationTarget.buildSettings ?? {}
|
||||
),
|
||||
appVersion: await readShortVersionAsync(
|
||||
projectDir,
|
||||
exp,
|
||||
applicationTarget.buildSettings ?? {}
|
||||
),
|
||||
appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
|
||||
appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
|
||||
};
|
||||
} catch (err: any) {
|
||||
Log.warn('Failed to read app versions.');
|
||||
Log.debug(err);
|
||||
Log.warn(err.message);
|
||||
Log.warn('Proceeding anyway...');
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function writeVersionsToInfoPlistAsync({
|
||||
projectDir,
|
||||
exp,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<IOSConfig.InfoPlist> {
|
||||
let updatedInfoPlist = IOSConfig.Version.setVersion(exp, infoPlist);
|
||||
updatedInfoPlist = IOSConfig.Version.setBuildNumber(exp, updatedInfoPlist);
|
||||
await writeInfoPlistAsync({ projectDir, infoPlist: updatedInfoPlist, buildSettings });
|
||||
return updatedInfoPlist;
|
||||
}
|
||||
|
||||
export function getInfoPlistPath(
|
||||
projectDir: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
@@ -182,66 +174,30 @@ async function readInfoPlistAsync(
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): Promise<IOSConfig.InfoPlist> {
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
|
||||
return (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
}
|
||||
|
||||
export async function updateNativeVersionsAsync({
|
||||
async function writeInfoPlistAsync({
|
||||
projectDir,
|
||||
version,
|
||||
buildNumber,
|
||||
targets,
|
||||
infoPlist,
|
||||
buildSettings,
|
||||
}: {
|
||||
projectDir: string;
|
||||
version?: string;
|
||||
buildNumber?: string;
|
||||
targets: Target[];
|
||||
infoPlist: IOSConfig.InfoPlist;
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
}): Promise<void> {
|
||||
const project = IOSConfig.XcodeUtils.getPbxproj(projectDir);
|
||||
const iosDir = path.join(projectDir, 'ios');
|
||||
const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
|
||||
await writePlistAsync(infoPlistPath, infoPlist);
|
||||
}
|
||||
|
||||
const infoPlistFiles: string[] = [];
|
||||
for (const target of targets) {
|
||||
const { targetName, buildConfiguration } = target;
|
||||
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
|
||||
targetName,
|
||||
buildConfiguration,
|
||||
});
|
||||
const infoPlist = xcBuildConfiguration?.buildSettings?.INFOPLIST_FILE;
|
||||
if (infoPlist) {
|
||||
const evaluatedInfoPlistPath = trimQuotes(
|
||||
evaluateTemplateString(infoPlist, {
|
||||
SRCROOT: iosDir,
|
||||
})
|
||||
);
|
||||
const absolutePath = path.isAbsolute(evaluatedInfoPlistPath)
|
||||
? evaluatedInfoPlistPath
|
||||
: path.join(iosDir, evaluatedInfoPlistPath);
|
||||
infoPlistFiles.push(path.normalize(absolutePath));
|
||||
}
|
||||
}
|
||||
const uniqueInfoPlistPaths = uniqBy(infoPlistFiles, i => i);
|
||||
for (const infoPlistPath of uniqueInfoPlistPaths) {
|
||||
const infoPlist = (await readPlistAsync(infoPlistPath)) as IOSConfig.InfoPlist;
|
||||
if (buildNumber) {
|
||||
infoPlist.CFBundleVersion = buildNumber;
|
||||
}
|
||||
if (version) {
|
||||
infoPlist.CFBundleShortVersionString = version;
|
||||
}
|
||||
await writePlistAsync(infoPlistPath, infoPlist);
|
||||
function ensureStaticConfigExists(projectDir: string): void {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
if (!paths.staticConfigPath) {
|
||||
throw new Error('autoIncrement option is not supported when using app.config.js');
|
||||
}
|
||||
}
|
||||
|
||||
function trimQuotes(s: string): string {
|
||||
return s?.startsWith('"') && s.endsWith('"') ? s.slice(1, -1) : s;
|
||||
}
|
||||
|
||||
export function evaluateTemplateString(
|
||||
s: string,
|
||||
buildSettings: XCBuildConfiguration['buildSettings']
|
||||
): string {
|
||||
// necessary because XCBuildConfiguration['buildSettings'] is not a plain object
|
||||
const vars = { ...buildSettings };
|
||||
export function evaluateTemplateString(s: string, vars: Record<string, any>): string {
|
||||
return s.replace(/\$\((\w+)\)/g, (match, key) => {
|
||||
if (vars.hasOwnProperty(key)) {
|
||||
const value = String(vars[key]);
|
||||
@@ -251,110 +207,3 @@ export function evaluateTemplateString(
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns buildNumber that will be used for the next build. If current build profile
|
||||
* has an 'autoIncrement' option set, it increments the version on server.
|
||||
*/
|
||||
export async function resolveRemoteBuildNumberAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
applicationTarget,
|
||||
buildProfile,
|
||||
}: {
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
applicationTarget: Target;
|
||||
buildProfile: BuildProfile<Platform.IOS>;
|
||||
}
|
||||
): Promise<string> {
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
AppPlatform.Ios,
|
||||
applicationTarget.bundleIdentifier
|
||||
);
|
||||
|
||||
const localBuildNumber = await readBuildNumberAsync(
|
||||
projectDir,
|
||||
exp,
|
||||
applicationTarget.buildSettings ?? {}
|
||||
);
|
||||
const localShortVersion = await readShortVersionAsync(
|
||||
projectDir,
|
||||
exp,
|
||||
applicationTarget.buildSettings ?? {}
|
||||
);
|
||||
let currentBuildVersion: string;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localBuildNumber) {
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
|
||||
);
|
||||
currentBuildVersion = localBuildNumber;
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
);
|
||||
throw new Error('Remote versions are not configured.');
|
||||
}
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
const spinner = ora(
|
||||
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
try {
|
||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
platform: AppPlatform.Ios,
|
||||
applicationIdentifier: applicationTarget.bundleIdentifier,
|
||||
storeVersion: localShortVersion ?? '1.0.0',
|
||||
buildVersion: currentBuildVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
|
||||
});
|
||||
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
|
||||
} catch (err) {
|
||||
spinner.fail(`Failed to initialize buildNumber with ${chalk.bold(currentBuildVersion)}.`);
|
||||
throw err;
|
||||
}
|
||||
return currentBuildVersion;
|
||||
} else {
|
||||
const nextBuildVersion = getNextBuildNumber(currentBuildVersion);
|
||||
const spinner = ora(
|
||||
`Incrementing buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
).start();
|
||||
try {
|
||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
platform: AppPlatform.Ios,
|
||||
applicationIdentifier: applicationTarget.bundleIdentifier,
|
||||
storeVersion: localShortVersion ?? '1.0.0',
|
||||
buildVersion: nextBuildVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
|
||||
});
|
||||
spinner.succeed(
|
||||
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
);
|
||||
} catch (err) {
|
||||
spinner.fail(
|
||||
`Failed to increment buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||
nextBuildVersion
|
||||
)}.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
return nextBuildVersion;
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,45 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { Job } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { ora } from '../ora';
|
||||
import Log from '../log';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.128';
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
* Local build that users can run on their own machines. Instead
|
||||
* of sending build request to EAS Servers it's passing it as an argument
|
||||
* to local-build-plugin, that will run the build locally.
|
||||
*
|
||||
* Triggered when running `eas build --local`.
|
||||
*/
|
||||
LOCAL_BUILD_PLUGIN = 'local-build-plugin',
|
||||
/**
|
||||
* Type of local build that is not accessible to users directly. When
|
||||
* cloud build is triggered by git based integration, we are running
|
||||
* in this mode. Instead of sending build request to EAS Servers it's
|
||||
* printing it to the stdout as JSON, so EAS Build worker can read it.
|
||||
*/
|
||||
INTERNAL = 'internal',
|
||||
}
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
localBuildMode?: LocalBuildMode;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
artifactPath?: string;
|
||||
workingdir?: string;
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
export async function runLocalBuildAsync(
|
||||
job: Job,
|
||||
metadata: Metadata,
|
||||
options: LocalBuildOptions
|
||||
): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job, metadata);
|
||||
let spinner;
|
||||
if (!options.verbose) {
|
||||
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
|
||||
}
|
||||
let childProcess: ChildProcess | undefined;
|
||||
const interruptHandler = (): void => {
|
||||
if (childProcess) {
|
||||
childProcess.kill();
|
||||
}
|
||||
};
|
||||
process.on('SIGINT', interruptHandler);
|
||||
export async function runLocalBuildAsync(job: Job): Promise<void> {
|
||||
const execNameOrPath =
|
||||
process.env.EAS_LOCAL_BUILD_PLUGIN_PATH ??
|
||||
(await findWithNodeResolutionAsync()) ??
|
||||
PLUGIN_PACKAGE_NAME;
|
||||
try {
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: options.verbose ? 'inherit' : 'pipe',
|
||||
env: {
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
|
||||
},
|
||||
const arg = Buffer.from(JSON.stringify({ job })).toString('base64');
|
||||
await spawnAsync(execNameOrPath, [arg], {
|
||||
stdio: 'inherit',
|
||||
});
|
||||
childProcess = spawnPromise.child;
|
||||
await spawnPromise;
|
||||
} finally {
|
||||
process.removeListener('SIGINT', interruptHandler);
|
||||
spinner?.stop();
|
||||
}
|
||||
}
|
||||
|
||||
async function getCommandAndArgsAsync(
|
||||
job: Job,
|
||||
metadata: Metadata
|
||||
): Promise<{ command: string; args: string[] }> {
|
||||
const jobAndMetadataBase64 = Buffer.from(JSON.stringify({ job, metadata })).toString('base64');
|
||||
if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
|
||||
return {
|
||||
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
|
||||
args: [jobAndMetadataBase64],
|
||||
};
|
||||
} else {
|
||||
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobAndMetadataBase64];
|
||||
if (await isAtLeastNpm7Async()) {
|
||||
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without
|
||||
// prompting the user to install a package that is used for the first time
|
||||
args.unshift('-y');
|
||||
} catch (err: any) {
|
||||
if (err.code === 'ENOENT') {
|
||||
Log.warn(`Could not resolve executable ${execNameOrPath}.`);
|
||||
Log.warn(
|
||||
`Install ${PLUGIN_PACKAGE_NAME} package from npm e.g. ${chalk.bold(
|
||||
`npm install -g ${PLUGIN_PACKAGE_NAME}`
|
||||
)} and make sure it's in PATH.`
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
return {
|
||||
command: 'npx',
|
||||
args,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function isAtLeastNpm7Async(): Promise<boolean> {
|
||||
const version = (await spawnAsync('npm', ['--version'])).stdout.trim();
|
||||
return semver.gte(version, '7.0.0');
|
||||
async function findWithNodeResolutionAsync(): Promise<string | undefined> {
|
||||
try {
|
||||
const resolvedPath = path.resolve(
|
||||
path.dirname(require.resolve(PLUGIN_PACKAGE_NAME)),
|
||||
'../bin/run'
|
||||
);
|
||||
if (await fs.pathExists(resolvedPath)) {
|
||||
return resolvedPath;
|
||||
}
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,107 +1,106 @@
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import Log from '../log';
|
||||
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
|
||||
import { getApplicationIdAsync } from '../project/android/applicationId';
|
||||
import { GradleBuildContext } from '../project/android/gradle';
|
||||
import { getBundleIdentifierAsync } from '../project/ios/bundleIdentifier';
|
||||
import { getUsername } from '../project/projectUtils';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from '../update/android/UpdatesModule';
|
||||
} from './android/UpdatesModule';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from '../update/ios/UpdatesModule';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||
import { BuildContext } from './context';
|
||||
} from './ios/UpdatesModule';
|
||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||
import { isExpoUpdatesInstalled } from './utils/updates';
|
||||
|
||||
export type MetadataContext<T extends Platform> = T extends Platform.ANDROID
|
||||
? AndroidMetadataContext
|
||||
: IosMetadataContext;
|
||||
|
||||
export interface AndroidMetadataContext {
|
||||
gradleContext?: GradleBuildContext;
|
||||
}
|
||||
export interface IosMetadataContext {
|
||||
buildSettings: XCBuildConfiguration['buildSettings'];
|
||||
targetName?: string;
|
||||
buildConfiguration?: string;
|
||||
}
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
): Promise<Metadata> {
|
||||
const vcsClient = getVcsClient();
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution =
|
||||
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
|
||||
? 'simulator'
|
||||
: ctx.buildProfile.distribution) ?? 'store';
|
||||
const metadata: Metadata = {
|
||||
trackingContext: ctx.analyticsEventProperties,
|
||||
...(await maybeResolveVersionsAsync(ctx)),
|
||||
const metadata = {
|
||||
trackingContext: ctx.trackingCtx,
|
||||
...(await maybeResolveVersionsAsync(ctx, platformContext)),
|
||||
cliVersion: easCliVersion,
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
||||
runtimeVersion: ctx.exp.runtimeVersion,
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
appIdentifier: await resolveAppIdentifierAsync(ctx, platformContext),
|
||||
buildProfile: ctx.buildProfileName,
|
||||
gitCommitHash: await vcsClient.getCommitHashAsync(),
|
||||
gitCommitMessage: truncateGitCommitMessage(
|
||||
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
|
||||
),
|
||||
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, ctx.user),
|
||||
message: ctx.message,
|
||||
gitCommitHash: await getVcsClient().getCommitHashAsync(),
|
||||
isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
|
||||
username: getUsername(ctx.exp, await ensureLoggedInAsync()),
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
ctx as BuildContext<Platform.IOS>
|
||||
),
|
||||
}),
|
||||
runWithNoWaitFlag: ctx.noWait,
|
||||
runFromCI: ctx.runFromCI,
|
||||
};
|
||||
return sanitizeMetadata(metadata);
|
||||
}
|
||||
|
||||
async function maybeResolveVersionsAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
): Promise<{ appBuildVersion?: string; appVersion?: string }> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
const iosContext = ctx as BuildContext<Platform.IOS>;
|
||||
const resolvedVersion = await maybeResolveIosVersionsAsync(
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
return await maybeResolveIosVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
iosContext.ios.targets
|
||||
iosContext?.buildSettings ?? {}
|
||||
);
|
||||
if (iosContext.ios.buildNumberOverride) {
|
||||
return {
|
||||
...resolvedVersion,
|
||||
appBuildVersion: iosContext.ios.buildNumberOverride,
|
||||
};
|
||||
}
|
||||
return resolvedVersion;
|
||||
} else if (ctx.platform === Platform.ANDROID) {
|
||||
const androidCtx = ctx as BuildContext<Platform.ANDROID>;
|
||||
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
|
||||
ctx.projectDir,
|
||||
ctx.exp,
|
||||
androidCtx.buildProfile
|
||||
);
|
||||
if (androidCtx.android.versionCodeOverride) {
|
||||
return {
|
||||
...resolvedVersion,
|
||||
appBuildVersion: androidCtx.android.versionCodeOverride,
|
||||
};
|
||||
}
|
||||
return resolvedVersion;
|
||||
return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
|
||||
} else {
|
||||
throw new Error(`Unsupported platform ${ctx.platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string {
|
||||
async function resolveAppIdentifierAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
platformContext: MetadataContext<T>
|
||||
): Promise<string> {
|
||||
if (ctx.platform === Platform.IOS) {
|
||||
return (ctx as BuildContext<Platform.IOS>).ios.bundleIdentifier;
|
||||
const iosContext = platformContext as IosMetadataContext;
|
||||
return await getBundleIdentifierAsync(ctx.projectDir, ctx.exp, {
|
||||
targetName: iosContext.targetName,
|
||||
buildConfiguration: iosContext.buildConfiguration,
|
||||
});
|
||||
} else {
|
||||
return (ctx as BuildContext<Platform.ANDROID>).android.applicationId;
|
||||
const androidContext = platformContext as AndroidMetadataContext;
|
||||
return await getApplicationIdAsync(ctx.projectDir, ctx.exp, androidContext.gradleContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,29 +154,8 @@ async function getNativeChannelAsync<T extends Platform>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getReactNativeVersionAsync(projectDir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const reactNativePackageJsonPath = resolveFrom(projectDir, 'react-native/package.json');
|
||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||
} catch (err) {
|
||||
Log.debug('Failed to resolve react-native version:');
|
||||
Log.debug(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveIosEnterpriseProvisioning(
|
||||
ctx: BuildContext<Platform.IOS>
|
||||
): IosEnterpriseProvisioning | undefined {
|
||||
return ctx.buildProfile.enterpriseProvisioning;
|
||||
}
|
||||
|
||||
export function truncateGitCommitMessage(
|
||||
msg: string | undefined,
|
||||
maxLength: number = 4096
|
||||
): string | undefined {
|
||||
if (msg === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return msg.length > maxLength ? `${msg.substring(0, maxLength - 3)}...` : msg;
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log from '../log';
|
||||
import { fromNow } from '../utils/date';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { formatGraphQLBuild } from './utils/formatBuild';
|
||||
|
||||
export const BUILDS_LIMIT = 50;
|
||||
|
||||
export async function listAndRenderBuildsOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
projectDisplayName,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
projectDisplayName: string;
|
||||
filter?: BuildFilter;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
filter,
|
||||
});
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
filter,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more builds?',
|
||||
renderListItems: builds =>
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAndSelectBuildOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
title,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
selectPromptDisabledFunction,
|
||||
selectPromptWarningMessage,
|
||||
}: {
|
||||
projectId: string;
|
||||
title: string;
|
||||
filter?: BuildFilter;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
|
||||
selectPromptWarningMessage?: string;
|
||||
}
|
||||
): Promise<BuildFragment | null> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a build in non-interactive mode.');
|
||||
} else {
|
||||
const selectedBuild = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit,
|
||||
offset,
|
||||
filter,
|
||||
}),
|
||||
promptOptions: {
|
||||
title,
|
||||
getIdentifierForQueryItem: build => build.id,
|
||||
makePartialChoiceObject: createBuildToPartialChoiceMaker(selectPromptDisabledFunction),
|
||||
selectPromptWarningMessage,
|
||||
},
|
||||
});
|
||||
return selectedBuild ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
function createBuildToPartialChoiceMaker(
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
|
||||
) {
|
||||
return (
|
||||
build: BuildFragment
|
||||
): {
|
||||
title: string;
|
||||
description: string;
|
||||
disabled?: boolean;
|
||||
} => {
|
||||
const splitCommitMessage = build.gitCommitMessage?.split('\n');
|
||||
const formattedCommitData =
|
||||
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
|
||||
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
|
||||
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
|
||||
)}"`
|
||||
: null;
|
||||
|
||||
const descriptionItems: { name: string; value: string | null }[] = [
|
||||
{ name: 'Version', value: build.appVersion ? chalk.bold(build.appVersion) : null },
|
||||
{ name: 'Commit', value: formattedCommitData },
|
||||
{
|
||||
name: 'Message',
|
||||
value: build.message
|
||||
? chalk.bold(
|
||||
build.message.length > 200 ? `${build.message.slice(0, 200)}...` : build.message
|
||||
)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
name: build.platform === AppPlatform.Ios ? 'Build number' : 'Version code',
|
||||
value: build.appBuildVersion ? chalk.bold(build.appBuildVersion) : null,
|
||||
},
|
||||
];
|
||||
|
||||
const filteredDescriptionArray: string[] = descriptionItems
|
||||
.filter(item => item.value)
|
||||
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
|
||||
|
||||
return {
|
||||
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
|
||||
`${fromNow(new Date(build.completedAt))} ago`
|
||||
)})`,
|
||||
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
|
||||
disabled: selectPromptDisabledFunction?.(build),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLatestBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
filter,
|
||||
}: {
|
||||
projectId: string;
|
||||
filter?: BuildFilter;
|
||||
}
|
||||
): Promise<BuildFragment | null> {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit: 1,
|
||||
offset: 0,
|
||||
filter,
|
||||
});
|
||||
|
||||
if (builds.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return builds[0];
|
||||
}
|
||||
|
||||
function renderPageOfBuilds({
|
||||
builds,
|
||||
projectDisplayName,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
builds: BuildFragment[];
|
||||
projectDisplayName: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): void {
|
||||
if (paginatedQueryOptions.json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
const list = builds.map(build => formatGraphQLBuild(build)).join(`\n\n${chalk.dim('———')}\n\n`);
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(`Builds for ${projectDisplayName}:`));
|
||||
Log.log(`\n${list}`);
|
||||
}
|
||||
}
|
||||
@@ -1,410 +0,0 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
AppVersionSource,
|
||||
BuildProfile,
|
||||
EasJson,
|
||||
EasJsonAccessor,
|
||||
EasJsonUtils,
|
||||
ResourceClass,
|
||||
SubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { Analytics } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildStatus,
|
||||
BuildWithSubmissionsFragment,
|
||||
SubmissionFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
|
||||
import {
|
||||
validateAppConfigForRemoteVersionSource,
|
||||
validateBuildProfileVersionSettings,
|
||||
} from '../project/remoteVersionSource';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { runAsync } from '../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
|
||||
submitAsync,
|
||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
|
||||
import { Actor } from '../user/User';
|
||||
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
|
||||
let metroConfigValidated = false;
|
||||
let sdkVersionChecked = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
resourceClass?: ResourceClass;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn
|
||||
): Promise<void> {
|
||||
await getVcsClient().ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||
|
||||
await ensureProjectConfiguredAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
const platforms = toPlatforms(flags.requestedPlatform);
|
||||
const buildProfiles = await getProfilesAsync({
|
||||
type: 'build',
|
||||
easJsonAccessor,
|
||||
platforms,
|
||||
profileName: flags.profile ?? undefined,
|
||||
});
|
||||
|
||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfiles,
|
||||
});
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||
}
|
||||
|
||||
const startedBuilds: {
|
||||
build: BuildWithSubmissionsFragment | BuildFragment;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
}[] = [];
|
||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
}
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.localBuildMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||
Log.newLine();
|
||||
|
||||
const submissions: SubmissionFragment[] = [];
|
||||
if (flags.autoSubmit) {
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
easJsonAccessor,
|
||||
platforms,
|
||||
profileName: flags.submitProfile,
|
||||
type: 'submit',
|
||||
});
|
||||
for (const startedBuild of startedBuilds) {
|
||||
const submitProfile = nullthrows(
|
||||
submitProfiles.find(
|
||||
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
|
||||
)
|
||||
).profile;
|
||||
const submission = await prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile.profile,
|
||||
submitProfile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
|
||||
graphqlClient,
|
||||
startedBuild.build.id
|
||||
);
|
||||
submissions.push(submission);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
printSubmissionDetailsUrls(submissions);
|
||||
Log.newLine();
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
||||
const builds = await waitForBuildEndAsync(graphqlClient, {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
accountName,
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
if (!flags.json) {
|
||||
printBuildResults(builds);
|
||||
}
|
||||
|
||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||
);
|
||||
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
|
||||
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
}
|
||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||
} else {
|
||||
const completedSubmissions = await waitForSubmissionsToCompleteAsync(
|
||||
graphqlClient,
|
||||
submissions
|
||||
);
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(
|
||||
await Promise.all(
|
||||
builds
|
||||
.filter((i): i is BuildWithSubmissionsFragment => !!i)
|
||||
.map(build => BuildQuery.withSubmissionsByIdAsync(graphqlClient, build.id))
|
||||
)
|
||||
);
|
||||
}
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
resourceClassFlag: flags.resourceClass,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
noWait: !flags.wait,
|
||||
platform: buildProfile.platform,
|
||||
projectDir,
|
||||
localBuildOptions: flags.localBuildOptions,
|
||||
easJsonCliConfig,
|
||||
message: flags.message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
getDynamicProjectConfigAsync,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[appPlatform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[appPlatform]} build`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
await validateBuildProfileConfigMatchesProjectConfigAsync(
|
||||
buildCtx.exp,
|
||||
buildProfile,
|
||||
buildCtx.projectId,
|
||||
flags.nonInteractive
|
||||
);
|
||||
|
||||
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
|
||||
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
|
||||
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
|
||||
}
|
||||
if (buildCtx.workflow === Workflow.MANAGED) {
|
||||
if (!sdkVersionChecked) {
|
||||
await checkExpoSdkIsSupportedAsync(buildCtx);
|
||||
sdkVersionChecked = true;
|
||||
}
|
||||
if (!metroConfigValidated) {
|
||||
await validateMetroConfigForManagedWorkflowAsync(buildCtx);
|
||||
metroConfigValidated = true;
|
||||
}
|
||||
}
|
||||
|
||||
const build = await startBuildAsync(buildCtx);
|
||||
return {
|
||||
build,
|
||||
buildCtx,
|
||||
};
|
||||
}
|
||||
|
||||
async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragment | undefined> {
|
||||
let sendBuildRequestAsync: BuildRequestSender;
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
sendBuildRequestAsync = await prepareAndroidBuildAsync(ctx as BuildContext<Platform.ANDROID>);
|
||||
} else {
|
||||
sendBuildRequestAsync = await prepareIosBuildAsync(ctx as BuildContext<Platform.IOS>);
|
||||
}
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
async function prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
buildCtx,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
submitProfile,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
submitProfile: SubmitProfile;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const platform = toPlatform(build.platform);
|
||||
const submissionCtx = await createSubmissionContextAsync({
|
||||
platform,
|
||||
projectDir,
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
graphqlClient: buildCtx.graphqlClient,
|
||||
analytics: buildCtx.analytics,
|
||||
projectId: buildCtx.projectId,
|
||||
exp: buildCtx.exp,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${chalk.bold(
|
||||
`${appPlatformDisplayNames[build.platform]} submission`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
return await submitAsync(submissionCtx);
|
||||
}
|
||||
|
||||
function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | null)[]): void {
|
||||
const failedBuilds = (maybeBuilds.filter(i => i) as BuildFragment[]).filter(
|
||||
i => i.status === BuildStatus.Errored
|
||||
);
|
||||
if (failedBuilds.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
assert(build.artifacts?.applicationArchiveUrl);
|
||||
const buildPath = await downloadAndMaybeExtractAppAsync(
|
||||
build.artifacts.applicationArchiveUrl,
|
||||
build.platform
|
||||
);
|
||||
await runAsync(buildPath, build.platform);
|
||||
}
|
||||
|
||||
async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
builds: MaybeBuildFragment[],
|
||||
flags: BuildFlags
|
||||
): Promise<void> {
|
||||
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
|
||||
|
||||
if (simBuilds.length > 0 && !flags.autoSubmit && !flags.nonInteractive) {
|
||||
for (const simBuild of simBuilds) {
|
||||
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
|
||||
Log.newLine();
|
||||
const confirm = await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
|
||||
});
|
||||
if (confirm) {
|
||||
await downloadAndRunAsync(simBuild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,3 +12,5 @@ export enum BuildDistributionType {
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export type TrackingContext = Record<string, string | number | boolean>;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { logEvent } from '../../analytics';
|
||||
|
||||
export enum Event {
|
||||
BUILD_COMMAND = 'build cli build command',
|
||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||
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_SUCCESS = 'build cli credentials sync update local success',
|
||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||
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 default {
|
||||
logEvent(name: Event, properties: Record<string, any>) {
|
||||
logEvent(name, properties);
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { error } from '@oclif/errors';
|
||||
import chalk from 'chalk';
|
||||
import resolveFrom from 'resolve-from';
|
||||
|
||||
@@ -20,7 +21,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
}: {
|
||||
projectDir: string;
|
||||
nonInteractive?: boolean;
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
buildProfiles?: ProfileData<BuildProfile>[];
|
||||
}): Promise<void> {
|
||||
if (await isExpoDevClientInstalledAsync(projectDir)) {
|
||||
return;
|
||||
@@ -64,7 +65,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
if (install) {
|
||||
await installExpoDevClientAsync(projectDir, { nonInteractive });
|
||||
} else {
|
||||
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||
error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
@@ -83,7 +84,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||
initial: false,
|
||||
});
|
||||
if (!shouldContinue) {
|
||||
Errors.error('Come back later', { exit: 1 });
|
||||
error('Come back later', { exit: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
value: build.message,
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import indentString from 'indent-string';
|
||||
import qrcodeTerminal from 'qrcode-terminal';
|
||||
|
||||
@@ -11,59 +12,38 @@ import {
|
||||
EasBuildDeprecationInfo,
|
||||
EasBuildDeprecationInfoType,
|
||||
} from '../../graphql/generated';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { printJsonOnlyOutput } from '../../utils/json';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
function terminalLinkFallback(url: string, text: string): string {
|
||||
return `${text} (${url})`;
|
||||
}
|
||||
|
||||
const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) => string> = {
|
||||
EAS_BUILD_UNKNOWN_FASTLANE_ERROR: build =>
|
||||
`The ${link(getBuildLogsUrl(build, 'run-fastlane'), {
|
||||
text: '"Run fastlane"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-fastlane'), '"Run fastlane"'),
|
||||
})} step failed with an unknown error. Refer to the ${link(
|
||||
getBuildLogsUrl(build, 'xcode-logs'),
|
||||
{
|
||||
text: '"Xcode logs"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'xcode-logs'), '"Xcode logs"'),
|
||||
}
|
||||
)} phase for additional, more detailed logs`,
|
||||
EAS_BUILD_UNKNOWN_GRADLE_ERROR: build =>
|
||||
`Gradle build failed with unknown error. See logs for the ${link(
|
||||
getBuildLogsUrl(build, 'run-gradlew'),
|
||||
{
|
||||
text: '"Run gradlew"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-gradlew'), '"Run gradlew"'),
|
||||
}
|
||||
)} phase for more information.`,
|
||||
};
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
|
||||
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} build details: ${link(logsUrl)}`
|
||||
} build details: ${chalk.underline(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput(builds);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +57,7 @@ function printBuildResult(build: BuildFragment): void {
|
||||
} build failed${userError ? ':' : ''}`
|
||||
);
|
||||
if (userError) {
|
||||
printUserError(userError, build);
|
||||
printUserError(userError);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -104,13 +84,13 @@ function printBuildResult(build: BuildFragment): void {
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} devices (or scan the QR code) to install the app:`
|
||||
);
|
||||
Log.log(`${link(logsUrl)}`);
|
||||
Log.log(`${chalk.underline(logsUrl)}`);
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${link(url)}`);
|
||||
Log.log(`${chalk.underline(url)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,34 +99,24 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
|
||||
return;
|
||||
}
|
||||
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) {
|
||||
Log.warn('This command is using API that soon will be deprecated. Upgrade EAS CLI.');
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn("Changes won't affect your project config.");
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) {
|
||||
Log.warn('This command is using API that soon will be deprecated.');
|
||||
Log.warn('This command is using API that soon will be deprecated, please update eas-cli.');
|
||||
Log.warn(
|
||||
'There might be some changes necessary to your project config, latest eas-cli will provide more specific error messages.'
|
||||
);
|
||||
Log.warn(deprecationInfo.message);
|
||||
} else {
|
||||
Log.warn('An unexpected warning was encountered. Report it as a bug:');
|
||||
Log.warn('An unexpected warning was encountered. Please report it as a bug:');
|
||||
Log.warn(deprecationInfo);
|
||||
}
|
||||
}
|
||||
|
||||
export function printUserError(error: BuildError, build: BuildFragment): void {
|
||||
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build);
|
||||
if (maybeErrorMessageOverride) {
|
||||
Log.error(maybeErrorMessageOverride);
|
||||
} else {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
export function printUserError(error: BuildError): void {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
}
|
||||
|
||||
function maybeGetErrorMessageOverride(error: BuildError, build: BuildFragment): string | null {
|
||||
if (!(error.errorCode in errorCodeToErrorMessageOverride)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return errorCodeToErrorMessageOverride[error.errorCode](build);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||
});
|
||||
|
||||
if (!answer) {
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,12 +39,12 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
|
||||
Log.log(
|
||||
`This operation needs to be run on a clean working tree. ${chalk.bold(
|
||||
'Commit all your changes before proceeding'
|
||||
`This operation needs to be run on a clean working tree, please ${chalk.bold(
|
||||
'commit all your changes before proceeding'
|
||||
)}.`
|
||||
);
|
||||
if (nonInteractive) {
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
const answer = await confirmAsync({
|
||||
message: `Commit changes to git?`,
|
||||
@@ -52,7 +52,7 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
|
||||
if (answer) {
|
||||
await commitPromptAsync({ commitAllFiles: true });
|
||||
} else {
|
||||
throw new Error('Commit all changes. Aborting...');
|
||||
throw new Error('Please commit all changes. Aborting...');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,11 +70,7 @@ export async function commitPromptAsync({
|
||||
initial: initialCommitMessage,
|
||||
validate: (input: string) => input !== '',
|
||||
});
|
||||
await getVcsClient().commitAsync({
|
||||
commitAllFiles,
|
||||
commitMessage: message,
|
||||
nonInteractive: false,
|
||||
});
|
||||
await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
|
||||
}
|
||||
|
||||
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||
@@ -140,7 +136,6 @@ export async function reviewAndCommitChangesAsync(
|
||||
await getVcsClient().commitAsync({
|
||||
commitMessage: initialCommitMessage,
|
||||
commitAllFiles: false,
|
||||
nonInteractive,
|
||||
});
|
||||
Log.withTick('Committed changes.');
|
||||
return;
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, ResourceClass } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import { BuildResourceClass } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getReactNativeVersionAsync } from '../metadata';
|
||||
|
||||
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
|
||||
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosM1Medium,
|
||||
[ResourceClass.M1_LARGE]: BuildResourceClass.IosM1Large,
|
||||
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
|
||||
};
|
||||
|
||||
const androidResourceClassToBuildResourceClassMapping: Record<
|
||||
Exclude<
|
||||
ResourceClass,
|
||||
| ResourceClass.M1_EXPERIMENTAL
|
||||
| ResourceClass.M1_MEDIUM
|
||||
| ResourceClass.M1_LARGE
|
||||
| ResourceClass.INTEL_MEDIUM
|
||||
>,
|
||||
BuildResourceClass
|
||||
> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
|
||||
};
|
||||
|
||||
export async function resolveBuildResourceClassAsync<T extends Platform>(
|
||||
profile: BuildProfile<T>,
|
||||
platform: Platform,
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
resourceClassFlag?: ResourceClass
|
||||
): Promise<BuildResourceClass> {
|
||||
const profileResourceClass = profile.resourceClass;
|
||||
|
||||
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
|
||||
Log.warn(
|
||||
`Build profile specifies the "${profileResourceClass}" resource class but you passed "${resourceClassFlag}" to --resource-class.\nUsing the "${resourceClassFlag}" as the override.`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedResourceClass = resourceClassFlag ?? profileResourceClass;
|
||||
|
||||
return platform === Platform.IOS
|
||||
? await resolveIosResourceClassAsync(exp, projectDir, resourceClassFlag ?? profileResourceClass)
|
||||
: resolveAndroidResourceClass(selectedResourceClass);
|
||||
}
|
||||
|
||||
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
|
||||
if (
|
||||
selectedResourceClass &&
|
||||
[
|
||||
ResourceClass.M1_EXPERIMENTAL,
|
||||
ResourceClass.M1_MEDIUM,
|
||||
ResourceClass.M1_LARGE,
|
||||
ResourceClass.INTEL_MEDIUM,
|
||||
].includes(selectedResourceClass)
|
||||
) {
|
||||
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
|
||||
}
|
||||
|
||||
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
|
||||
|
||||
return androidResourceClassToBuildResourceClassMapping[
|
||||
resourceClass as Exclude<
|
||||
ResourceClass,
|
||||
| ResourceClass.M1_EXPERIMENTAL
|
||||
| ResourceClass.M1_MEDIUM
|
||||
| ResourceClass.M1_LARGE
|
||||
| ResourceClass.INTEL_MEDIUM
|
||||
>
|
||||
];
|
||||
}
|
||||
|
||||
async function resolveIosResourceClassAsync(
|
||||
exp: ExpoConfig,
|
||||
projectDir: string,
|
||||
selectedResourceClass?: ResourceClass
|
||||
): Promise<BuildResourceClass> {
|
||||
const resourceClass =
|
||||
selectedResourceClass ?? (await resolveIosDefaultRequestedResourceClassAsync(exp, projectDir));
|
||||
|
||||
if (resourceClass === ResourceClass.M1_EXPERIMENTAL) {
|
||||
Log.warn(`Resource class ${chalk.bold('m1-experimental')} is deprecated.`);
|
||||
}
|
||||
|
||||
return iosResourceClassToBuildResourceClassMapping[resourceClass];
|
||||
}
|
||||
|
||||
async function resolveIosDefaultRequestedResourceClassAsync(
|
||||
exp: ExpoConfig,
|
||||
projectDir: string
|
||||
): Promise<ResourceClass> {
|
||||
const { sdkVersion } = exp;
|
||||
const reactNativeVersion = await getReactNativeVersionAsync(projectDir);
|
||||
if (
|
||||
(sdkVersion && semver.satisfies(sdkVersion, '>=48')) ||
|
||||
(reactNativeVersion && semver.satisfies(reactNativeVersion, '>=0.71.0'))
|
||||
) {
|
||||
return ResourceClass.M1_MEDIUM;
|
||||
} else {
|
||||
return ResourceClass.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ExpoConfig, getPackageJson } from '@expo/config';
|
||||
|
||||
export function isExpoUpdatesInstalled(projectDir: string): boolean {
|
||||
const packageJson = getPackageJson(projectDir);
|
||||
return !!(packageJson.dependencies && 'expo-updates' in packageJson.dependencies);
|
||||
}
|
||||
|
||||
export function ensureValidVersions(exp: ExpoConfig): void {
|
||||
if (!exp.runtimeVersion && !exp.sdkVersion) {
|
||||
throw new Error(
|
||||
"Couldn't find either 'runtimeVersion' or 'sdkVersion' to configure 'expo-updates'. Please specify at least one of these properties under the 'expo' key in 'app.json'"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,25 @@ import { getExpoApiBaseUrl, getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { AppPlatform, BuildFragment } from '../../graphql/generated';
|
||||
|
||||
export function getProjectDashboardUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/projects/${projectName}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${accountName}/projects/${projectName}`;
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string {
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
const { project } = build;
|
||||
const url =
|
||||
project.__typename !== 'App'
|
||||
? `/builds/${build.id}`
|
||||
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}${
|
||||
hash ? `#${hash}` : ''
|
||||
}`;
|
||||
|
||||
return new URL(url, getExpoWebsiteBaseUrl()).toString();
|
||||
if (project.__typename === 'App') {
|
||||
return `${getExpoWebsiteBaseUrl()}/accounts/${project.ownerAccount.name}/builds/${build.id}`;
|
||||
} else {
|
||||
return `${getExpoWebsiteBaseUrl()}/builds/${build.id}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function getArtifactUrl(artifactId: string): string {
|
||||
return new URL(`/artifacts/${artifactId}`, getExpoWebsiteBaseUrl()).toString();
|
||||
return `${getExpoWebsiteBaseUrl()}/artifacts/${artifactId}`;
|
||||
}
|
||||
|
||||
export function getInternalDistributionInstallUrl(build: BuildFragment): string {
|
||||
if (build.platform === AppPlatform.Ios) {
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/v2/projects/${
|
||||
return `itms-services://?action=download-manifest;url=${getExpoApiBaseUrl()}/--/api/v2/projects/${
|
||||
build.project.id
|
||||
}/builds/${build.id}/manifest.plist`;
|
||||
}
|
||||
@@ -37,16 +31,3 @@ export function getInternalDistributionInstallUrl(build: BuildFragment): string
|
||||
|
||||
return build.artifacts.buildUrl;
|
||||
}
|
||||
|
||||
export function getUpdateGroupUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
updateGroupId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/updates/${encodeURIComponent(updateGroupId)}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -1,15 +1,12 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Errors } from '@oclif/core';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
import { isPNGAsync } from '../utils/image';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { CommonContext } from './context';
|
||||
import { BuildContext } from './context';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
export function checkNodeEnvVariable(ctx: BuildContext<Platform>): void {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
@@ -19,7 +16,7 @@ export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
}
|
||||
|
||||
export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
ctx: BuildContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow === Workflow.GENERIC || ctx.buildProfile?.env?.GOOGLE_SERVICES_FILE) {
|
||||
return;
|
||||
@@ -53,137 +50,3 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
function isInsideDirectory(file: string, directory: string): boolean {
|
||||
return file.startsWith(directory);
|
||||
}
|
||||
|
||||
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
|
||||
ctx: CommonContext<T>
|
||||
): Promise<void> {
|
||||
if (ctx.workflow !== Workflow.MANAGED) {
|
||||
return;
|
||||
}
|
||||
|
||||
// don't run PNG checks on SDK 47 and newer
|
||||
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
|
||||
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ctx.platform === Platform.ANDROID) {
|
||||
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
|
||||
}
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// else {
|
||||
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
|
||||
// }
|
||||
}
|
||||
|
||||
type ConfigPng = { configPath: string; pngPath: string | undefined };
|
||||
|
||||
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
|
||||
const pngs: ConfigPng[] = [
|
||||
{
|
||||
configPath: 'exp.icon',
|
||||
pngPath: ctx.exp.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.icon',
|
||||
pngPath: ctx.exp.android?.icon,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.foregroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.android.adaptiveIcon.backgroundImage',
|
||||
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.splash.image',
|
||||
pngPath: ctx.exp.splash?.image,
|
||||
},
|
||||
{
|
||||
configPath: 'exp.notification.icon',
|
||||
pngPath: ctx.exp.notification?.icon,
|
||||
},
|
||||
];
|
||||
await validatePNGsAsync(pngs);
|
||||
}
|
||||
|
||||
// Validating iOS PNGs is currently disabled
|
||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
||||
//
|
||||
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
|
||||
// const pngs: ConfigPng[] = [
|
||||
// {
|
||||
// configPath: 'exp.icon',
|
||||
// pngPath: ctx.exp.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.ios.icon',
|
||||
// pngPath: ctx.exp.ios?.icon,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.splash.image',
|
||||
// pngPath: ctx.exp.splash?.image,
|
||||
// },
|
||||
// {
|
||||
// configPath: 'exp.notification.icon',
|
||||
// pngPath: ctx.exp.notification?.icon,
|
||||
// },
|
||||
// ];
|
||||
// await validatePNGsAsync(pngs);
|
||||
|
||||
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
|
||||
// if (!icon) {
|
||||
// return;
|
||||
// }
|
||||
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
|
||||
|
||||
// try {
|
||||
// await ensurePNGIsNotTransparentAsync(icon);
|
||||
// } catch (err: any) {
|
||||
// if (err instanceof ImageTransparencyError) {
|
||||
// Log.error(
|
||||
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
|
||||
// );
|
||||
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
|
||||
// Errors.exit(1);
|
||||
// } else {
|
||||
// throw err;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
|
||||
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
|
||||
const validationResults = await Promise.allSettled(validationPromises);
|
||||
const failedValidations = validationResults.filter(
|
||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
||||
);
|
||||
|
||||
if (failedValidations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.error('PNG images validation failed:');
|
||||
for (const { reason } of failedValidations) {
|
||||
const error: Error = reason;
|
||||
Log.error(`- ${error.message}`);
|
||||
}
|
||||
Errors.exit(1);
|
||||
}
|
||||
|
||||
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
|
||||
if (!pngPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pngPath.endsWith('.png')) {
|
||||
throw new Error(`"${configPath}" is not a PNG file`);
|
||||
}
|
||||
|
||||
if (!(await isPNGAsync(pngPath))) {
|
||||
throw new Error(`"${configPath}" is not valid PNG`);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export class ChannelNotFoundError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message ?? 'Channel not found.');
|
||||
}
|
||||
}
|
||||
@@ -1,264 +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 {
|
||||
CreateUpdateChannelOnAppMutation,
|
||||
CreateUpdateChannelOnAppMutationVariables,
|
||||
ViewBranchesOnUpdateChannelQueryVariables,
|
||||
ViewUpdateChannelsOnAppQueryVariables,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import formatFields from '../utils/formatFields';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
paginatedQueryWithConfirmPromptAsync,
|
||||
paginatedQueryWithSelectPromptAsync,
|
||||
} from '../utils/queries';
|
||||
import { logChannelDetails } from './utils';
|
||||
|
||||
export const CHANNELS_LIMIT = 25;
|
||||
|
||||
export async function selectChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
selectionPromptTitle,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
selectionPromptTitle: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<UpdateChannelObject> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a channel in non-interactive mode.');
|
||||
}
|
||||
|
||||
const updateChannel = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryChannelsOnAppAsync(graphqlClient, { appId: projectId, limit, offset }),
|
||||
promptOptions: {
|
||||
title: selectionPromptTitle,
|
||||
makePartialChoiceObject: updateChannel => ({ title: updateChannel.name }),
|
||||
getIdentifierForQueryItem: updateChannel => updateChannel.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!updateChannel) {
|
||||
throw new Error(`Could not find any channels for app "${projectId}"`);
|
||||
}
|
||||
return updateChannel;
|
||||
}
|
||||
|
||||
export async function listAndRenderChannelsOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const channels = await queryChannelsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
});
|
||||
renderPageOfChannels(channels, paginatedQueryOptions);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryChannelsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId: appId,
|
||||
channelName,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
channelName: string;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
});
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions);
|
||||
} else {
|
||||
await paginatedQueryWithConfirmPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? CHANNELS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
|
||||
channelName,
|
||||
appId,
|
||||
offset,
|
||||
limit,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: branches =>
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function queryChannelsOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, offset, limit }: ViewUpdateChannelsOnAppQueryVariables
|
||||
): Promise<UpdateChannelObject[]> {
|
||||
return await ChannelQuery.viewUpdateChannelsOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
offset,
|
||||
limit,
|
||||
});
|
||||
}
|
||||
|
||||
async function queryBranchesAndUpdateGroupsOnChannelAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
args: ViewBranchesOnUpdateChannelQueryVariables
|
||||
): Promise<UpdateBranchOnChannelObject[]> {
|
||||
return await BranchQuery.listBranchesOnChannelAsync(graphqlClient, args);
|
||||
}
|
||||
|
||||
function renderPageOfChannels(
|
||||
currentPage: UpdateChannelObject[],
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
Log.addNewLineIfNone();
|
||||
logChannelDetails(channel);
|
||||
|
||||
if (currentPage.indexOf(channel) < currentPage.length - 1) {
|
||||
Log.log(`\n${chalk.dim('———')}\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderPageOfBranchesOnChannel(
|
||||
channel: UpdateChannelObject,
|
||||
currentPage: UpdateBranchOnChannelObject[],
|
||||
{ json }: PaginatedQueryOptions
|
||||
): void {
|
||||
const channelWithNewBranches = { ...channel, updateBranches: currentPage };
|
||||
if (json) {
|
||||
printJsonOnlyOutput({ currentPage: channelWithNewBranches });
|
||||
} else {
|
||||
// The channel details contain both the branch and latest update group
|
||||
Log.addNewLineIfNone();
|
||||
logChannelDetails(channelWithNewBranches);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChannelHeaderContent({
|
||||
channelName,
|
||||
channelId,
|
||||
}: {
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
}): void {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: channelName },
|
||||
{ label: 'ID', value: channelId },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk`{bold Branches pointed at this channel and their most recent update group:}`);
|
||||
}
|
||||
|
||||
export async function createChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchId,
|
||||
channelName,
|
||||
}: {
|
||||
appId: string;
|
||||
branchId: string;
|
||||
channelName: string;
|
||||
}
|
||||
): Promise<CreateUpdateChannelOnAppMutation> {
|
||||
// Point the new channel at a branch with its same name.
|
||||
const branchMapping = JSON.stringify({
|
||||
data: [{ branchId, branchMappingLogic: 'true' }],
|
||||
version: 0,
|
||||
});
|
||||
return await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateChannelOnAppMutation, CreateUpdateChannelOnAppMutationVariables>(
|
||||
gql`
|
||||
mutation CreateUpdateChannelOnApp($appId: ID!, $name: String!, $branchMapping: String!) {
|
||||
updateChannel {
|
||||
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
|
||||
id
|
||||
name
|
||||
branchMapping
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name: channelName,
|
||||
branchMapping,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
}
|
||||
|
||||
export async function ensureChannelExistsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, branchId, channelName }: { appId: string; branchId: string; channelName: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
await createChannelOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
branchId,
|
||||
});
|
||||
} catch (e: any) {
|
||||
const isIgnorableError =
|
||||
e.graphQLErrors?.length === 1 &&
|
||||
e.graphQLErrors[0].extensions.errorCode === 'CHANNEL_ALREADY_EXISTS';
|
||||
if (!isIgnorableError) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import Log from '../log';
|
||||
import {
|
||||
FormattedBranchDescription,
|
||||
formatBranch,
|
||||
getUpdateGroupDescriptionsWithBranch,
|
||||
} from '../update/utils';
|
||||
|
||||
export type BranchMapping = {
|
||||
version: number;
|
||||
data: {
|
||||
branchId: string;
|
||||
branchMappingLogic: {
|
||||
operand: number;
|
||||
clientKey: string;
|
||||
branchMappingOperator: string;
|
||||
} & string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the branch mapping and determine whether it is a rollout.
|
||||
* Ensure that the branch mapping is properly formatted.
|
||||
*/
|
||||
export function getBranchMapping(branchMappingString?: string): {
|
||||
branchMapping: BranchMapping;
|
||||
isRollout: boolean;
|
||||
rolloutPercent?: number;
|
||||
} {
|
||||
if (!branchMappingString) {
|
||||
throw new Error('Missing branch mapping.');
|
||||
}
|
||||
let branchMapping: BranchMapping;
|
||||
try {
|
||||
branchMapping = JSON.parse(branchMappingString);
|
||||
} catch {
|
||||
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
|
||||
}
|
||||
assert(branchMapping, 'Branch Mapping must be defined.');
|
||||
|
||||
if (branchMapping.version !== 0) {
|
||||
throw new Error('Branch mapping must be version 0.');
|
||||
}
|
||||
|
||||
const isRollout = branchMapping.data.length === 2;
|
||||
const rolloutPercent = branchMapping.data[0]?.branchMappingLogic.operand;
|
||||
|
||||
switch (branchMapping.data.length) {
|
||||
case 0:
|
||||
break;
|
||||
case 1:
|
||||
if (branchMapping.data[0].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a single branch must be "true"');
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
|
||||
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
|
||||
}
|
||||
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
|
||||
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
|
||||
}
|
||||
if (rolloutPercent == null) {
|
||||
throw new Error('Branch mapping is missing a "rolloutPercent"');
|
||||
}
|
||||
if (branchMapping.data[1].branchMappingLogic !== 'true') {
|
||||
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error('Branch mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
return { branchMapping, isRollout, rolloutPercent };
|
||||
}
|
||||
|
||||
export function logChannelDetails(channel: UpdateChannelObject): void {
|
||||
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
|
||||
if (branchMapping.data.length > 2) {
|
||||
throw new Error('Branch Mapping data must have length less than or equal to 2.');
|
||||
}
|
||||
|
||||
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
|
||||
const branchDescription = channel.updateBranches.flatMap(branch => {
|
||||
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
|
||||
branch.updateGroups
|
||||
);
|
||||
|
||||
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
|
||||
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
|
||||
let rolloutPercentNumber: number | undefined = undefined;
|
||||
if (isRolloutBranch) {
|
||||
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
|
||||
}
|
||||
|
||||
return updateGroupWithBranchDescriptions.map(
|
||||
({ branch, ...updateGroup }): FormattedBranchDescription => ({
|
||||
branch,
|
||||
branchRolloutPercentage: rolloutPercentNumber,
|
||||
update: updateGroup,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (branchDescription.length === 0) {
|
||||
Log.log(chalk.dim('No branches are pointed to this channel.'));
|
||||
} else {
|
||||
Log.log(
|
||||
branchDescription
|
||||
.map(description => formatBranch(description))
|
||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,178 +1,69 @@
|
||||
import { Command } from '@oclif/core';
|
||||
import nullthrows from 'nullthrows';
|
||||
import { EasJsonReader } from '@expo/eas-json';
|
||||
import { Command } from '@oclif/command';
|
||||
import semver from 'semver';
|
||||
|
||||
import {
|
||||
AnalyticsWithOrchestration,
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from './context/LoggedInContextField';
|
||||
import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
|
||||
import { OptionalProjectConfigContextField } from './context/OptionalProjectConfigContextField';
|
||||
import ProjectConfigContextField from './context/ProjectConfigContextField';
|
||||
import ProjectDirContextField from './context/ProjectDirContextField';
|
||||
import SessionManagementContextField from './context/SessionManagementContextField';
|
||||
|
||||
type ContextInput<
|
||||
T extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
> = {
|
||||
[P in keyof T]: ContextField<T[P]>;
|
||||
};
|
||||
|
||||
type ContextOutput<
|
||||
T extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
> = {
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
AnalyticsEvent,
|
||||
flushAsync as flushAnalyticsAsync,
|
||||
initAsync as initAnalyticsAsync,
|
||||
logEvent,
|
||||
} from '../analytics';
|
||||
import Log from '../log';
|
||||
import { findProjectRootAsync } from '../project/projectUtils';
|
||||
import { getUserAsync } from '../user/User';
|
||||
import { ensureLoggedInAsync } from '../user/actions';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { setVcsClient } from '../vcs';
|
||||
import GitClient from '../vcs/clients/git';
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
protected static readonly ContextOptions = {
|
||||
/**
|
||||
* Require this command to be run when logged-in. Returns the logged-in actor and a logged-in
|
||||
* graphql client in the context.
|
||||
*/
|
||||
LoggedIn: {
|
||||
loggedIn: new LoggedInContextField(),
|
||||
},
|
||||
/**
|
||||
* Do not require this command to be run when logged-in, but if it is get the logged-in actor and a
|
||||
* maybe-logged-in graphql client.
|
||||
*/
|
||||
MaybeLoggedIn: {
|
||||
maybeLoggedIn: new MaybeLoggedInContextField(),
|
||||
},
|
||||
/**
|
||||
* Specify this context requirement if the command needs to mutate the user session.
|
||||
*/
|
||||
SessionManagment: {
|
||||
sessionManager: new SessionManagementContextField(),
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server if this command is being
|
||||
* run within a project directory, null otherwise.
|
||||
*/
|
||||
OptionalProjectConfig: {
|
||||
projectConfig: new OptionalProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require this command to be run in a project directory. Return the project directory in the context.
|
||||
*/
|
||||
ProjectDir: {
|
||||
projectDir: new ProjectDirContextField(),
|
||||
},
|
||||
/**
|
||||
* Provides functions to load the project config when dynamic config options are needed (custom Env for example).
|
||||
*/
|
||||
DynamicProjectConfig: {
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
getDynamicProjectConfigAsync: new DynamicProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require the project to be identified and registered on server. Returns the project config in the context.
|
||||
* This also requires the user to be logged in (getProjectIdAsync requires logged in), so also expose that context.
|
||||
* Exposing the loggedIn context here helps us guarantee user identification for logging purposes.
|
||||
*/
|
||||
ProjectConfig: {
|
||||
loggedIn: new LoggedInContextField(),
|
||||
projectConfig: new ProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Analytics manager. Returns the analytics manager in the context for use by the command.
|
||||
*/
|
||||
Analytics: {
|
||||
analytics: new AnalyticsContextField(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Context allows for subclasses (commands) to declare their prerequisites in a type-safe manner.
|
||||
* These declarative definitions each output a context property that is the result of the prerequisite being
|
||||
* satisfied. These allow a unified common interface to be shared amongst commands in order to provide a more
|
||||
* consistent CLI experience.
|
||||
*
|
||||
* For example, let's say a command needs the EAS project ID to make a GraphQL mutation. It should declare that
|
||||
* it requires the `ProjectConfig` context, and then call `getContextAsync` to get the project ID.
|
||||
* When user data is unavailable locally, determines if the command will
|
||||
* force the user to log in
|
||||
*/
|
||||
static contextDefinition: ContextInput = {};
|
||||
|
||||
/**
|
||||
* Execute the context in the contextDefinition to satisfy command prerequisites.
|
||||
*/
|
||||
protected async getContextAsync<
|
||||
C extends {
|
||||
[name: string]: any;
|
||||
} = object
|
||||
>(
|
||||
commandClass: { contextDefinition: ContextInput<C> },
|
||||
{ nonInteractive }: { nonInteractive: boolean }
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
|
||||
// do these serially so that they don't do things like ask for login twice in parallel
|
||||
const contextValuePairs = [];
|
||||
for (const [contextKey, contextField] of Object.entries(contextDefinition)) {
|
||||
contextValuePairs.push([
|
||||
contextKey,
|
||||
await contextField.getValueAsync({
|
||||
nonInteractive,
|
||||
sessionManager: this.sessionManager,
|
||||
analytics: this.analytics,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return Object.fromEntries(contextValuePairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* The user session manager. Responsible for coordinating all user session related state.
|
||||
* If needed in a subclass, use the SessionManager ContextOption.
|
||||
*/
|
||||
private sessionManagerInternal?: SessionManager;
|
||||
private get sessionManager(): SessionManager {
|
||||
return nullthrows(this.sessionManagerInternal);
|
||||
}
|
||||
|
||||
/**
|
||||
* The analytics manager. Used for logging analytics.
|
||||
* It is set up here to ensure a consistent set up.
|
||||
*/
|
||||
private analyticsInternal?: AnalyticsWithOrchestration;
|
||||
private get analytics(): AnalyticsWithOrchestration {
|
||||
return nullthrows(this.analyticsInternal);
|
||||
}
|
||||
protected requiresAuthentication = true;
|
||||
|
||||
protected abstract runAsync(): Promise<any>;
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
async run(): Promise<any> {
|
||||
this.analyticsInternal = await createAnalyticsAsync();
|
||||
this.sessionManagerInternal = new SessionManager(this.analytics);
|
||||
EasJsonReader.setLog(Log);
|
||||
|
||||
// this is needed for logEvent call below as it identifies the user in the analytics system
|
||||
// if possible
|
||||
await this.sessionManager.getUserAsync();
|
||||
await initAnalyticsAsync();
|
||||
await this.applyCliConfigAsync();
|
||||
|
||||
this.analytics.logEvent(CommandEvent.ACTION, {
|
||||
if (this.requiresAuthentication) {
|
||||
const { flags } = this.parse();
|
||||
const nonInteractive = (flags as any)['non-interactive'] ?? false;
|
||||
await ensureLoggedInAsync({ nonInteractive });
|
||||
} else {
|
||||
await getUserAsync();
|
||||
}
|
||||
logEvent(AnalyticsEvent.ACTION, {
|
||||
// id is assigned by oclif in constructor based on the filepath:
|
||||
// commands/submit === submit, commands/build/list === build:list
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
|
||||
return this.runAsync();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
override async finally(err: Error): Promise<any> {
|
||||
await this.analytics.flushAsync();
|
||||
async finally(err: Error): Promise<any> {
|
||||
await flushAnalyticsAsync();
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
private async applyCliConfigAsync(): Promise<void> {
|
||||
const projectDir = await findProjectRootAsync();
|
||||
const easJsonReader = new EasJsonReader(projectDir);
|
||||
const config = await easJsonReader.getCliConfigAsync();
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
|
||||
);
|
||||
}
|
||||
if (config?.requireCommit) {
|
||||
setVcsClient(new GitClient());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import { asMock } from '../../__tests__/utils';
|
||||
import { flushAsync, initAsync, logEvent } from '../../analytics';
|
||||
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
|
||||
import { getUserAsync } from '../../user/User';
|
||||
import { ensureLoggedInAsync } from '../../user/actions';
|
||||
import EasCommand from '../EasCommand';
|
||||
import TestEasCommand from './TestEasCommand';
|
||||
|
||||
TestEasCommand.prototype.authValue = jest.fn().mockImplementation(() => true);
|
||||
|
||||
jest.mock('../../user/User');
|
||||
jest.mock('../../user/SessionManager');
|
||||
jest.mock('../../analytics/AnalyticsManager', () => {
|
||||
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager');
|
||||
jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
|
||||
jest.mock('../../analytics', () => {
|
||||
const { AnalyticsEvent } = jest.requireActual('../../analytics');
|
||||
return {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync: jest.fn(),
|
||||
AnalyticsEvent,
|
||||
logEvent: jest.fn(),
|
||||
initAsync: jest.fn(),
|
||||
flushAsync: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,27 +31,18 @@ afterAll(() => {
|
||||
process.argv = originalProcessArgv;
|
||||
});
|
||||
|
||||
const analytics: AnalyticsWithOrchestration = {
|
||||
logEvent: jest.fn((): void => {}),
|
||||
setActor: jest.fn((): void => {}),
|
||||
flushAsync: jest.fn(async (): Promise<void> => {}),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics);
|
||||
asMock(getUserAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(ensureLoggedInAsync)
|
||||
.mockReset()
|
||||
.mockImplementation(() => mockJester);
|
||||
asMock(initAsync).mockReset();
|
||||
asMock(flushAsync).mockReset();
|
||||
asMock(logEvent).mockReset();
|
||||
});
|
||||
|
||||
const createTestEasCommand = (): typeof EasCommand => {
|
||||
class TestEasCommand extends EasCommand {
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
return TestEasCommand;
|
||||
};
|
||||
|
||||
describe(EasCommand.name, () => {
|
||||
describe('without exceptions', () => {
|
||||
// The first test in this suite should have an increased timeout
|
||||
@@ -53,33 +52,36 @@ describe(EasCommand.name, () => {
|
||||
//
|
||||
// See https://github.com/oclif/command/blob/master/src/command.ts#L80
|
||||
// and look for "Config.load"
|
||||
it('ensures the user data is read', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
it('ensures the user is logged in', async () => {
|
||||
await TestEasCommand.run();
|
||||
|
||||
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync');
|
||||
expect(sessionManagerSpy).toBeCalledTimes(1);
|
||||
}, 30_000);
|
||||
expect(ensureLoggedInAsync).toHaveBeenCalled();
|
||||
}, 15_000);
|
||||
|
||||
it('ensures the user data is read from cache', async () => {
|
||||
asMock(TestEasCommand.prototype.authValue).mockImplementationOnce(() => false);
|
||||
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(getUserAsync).toHaveReturnedWith(mockJester);
|
||||
});
|
||||
|
||||
it('initializes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(createAnalyticsAsync).toHaveBeenCalled();
|
||||
expect(initAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logs events', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
await TestEasCommand.run();
|
||||
|
||||
expect(analytics.logEvent).toHaveBeenCalledWith('action', {
|
||||
expect(logEvent).toHaveBeenCalledWith('action', {
|
||||
action: `eas ${TestEasCommand.id}`,
|
||||
});
|
||||
});
|
||||
@@ -87,14 +89,13 @@ describe(EasCommand.name, () => {
|
||||
|
||||
describe('after exceptions', () => {
|
||||
it('flushes analytics', async () => {
|
||||
const TestEasCommand = createTestEasCommand();
|
||||
try {
|
||||
await TestEasCommand.run().then(() => {
|
||||
throw new Error('foo');
|
||||
});
|
||||
} catch {}
|
||||
} catch (error) {}
|
||||
|
||||
expect(analytics.flushAsync).toHaveBeenCalled();
|
||||
expect(flushAsync).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import EasCommand from '../EasCommand';
|
||||
|
||||
export default class TestEasCommand extends EasCommand {
|
||||
requiresAuthentication = this.authValue();
|
||||
|
||||
authValue(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {}
|
||||
}
|
||||
|
||||
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import ContextField, { ContextOptions } from './ContextField';
|
||||
|
||||
export default class AnalyticsContextField extends ContextField<Analytics> {
|
||||
async getValueAsync({ analytics }: ContextOptions): Promise<Analytics> {
|
||||
return analytics;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
|
||||
export interface ContextOptions {
|
||||
sessionManager: SessionManager;
|
||||
analytics: Analytics;
|
||||
nonInteractive: boolean;
|
||||
}
|
||||
|
||||
export default abstract class ContextField<T> {
|
||||
abstract getValueAsync(options: ContextOptions): Promise<T>;
|
||||
}
|
||||
Loaded 100 of 661 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user