Compare commits

..
Author SHA1 Message Date
Quinlan Jung a4eb9ff37b functions start 2022-05-10 17:27:48 -07:00
Quinlan Jung ddfe541af2 put deps into eascli 2022-05-10 17:27:48 -07:00
Quinlan Jung cde1ab36d2 last log is not always done 2022-05-10 17:27:48 -07:00
Quinlan Jung 087074d831 extra debug 2022-05-10 17:27:48 -07:00
Quinlan Jung 365e2d7e07 tmp 2022-05-10 17:27:48 -07:00
Quinlan Jung 1691f3271f tmp 2022-05-10 17:27:47 -07:00
625 changed files with 14857 additions and 49784 deletions

No files matched your search

-4
View File
@@ -1,5 +1 @@
* text eol=lf
*.png binary
*.jpg binary
**/yarn.lock linguist-generated **/yarn.lock linguist-generated
+1 -1
View File
@@ -37,7 +37,7 @@ body:
required: true required: true
attributes: attributes:
label: Environment label: Environment
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here. description: Run `npx expo-env-info` and paste the output here.
- type: textarea - type: textarea
validations: validations:
required: false required: false
@@ -1,4 +0,0 @@
{
"printWidth": 100,
"singleQuote": true
}
@@ -1,53 +0,0 @@
name: notify-slack-on-fail-or-recover
inputs:
channel:
required: true
type: string
slack_webhook:
required: true
type: string
runs:
using: 'composite'
steps:
- name: Get previous workflow run status
uses: actions/github-script@v6
id: run-status
with:
script: |
const script = require('.github/internal-actions/notify-slack-on-fail-or-recover/main.js');
return await script({github, context, core});
- name: Commit subject
shell: bash
run: |
subject="$(git show --format=%s --quiet | sed 's/"/\\"/g')"
echo "${{ fromJSON(steps.run-status.outputs.result).previous }}"
echo "SUBJECT=$subject" >> "$GITHUB_ENV"
- name: Send Slack Success Notification
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: good
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Fixed, was failing'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
- name: Send Slack Failure Notification
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
uses: rtCamp/action-slack-notify@v2
env:
SLACK_CHANNEL: ${{ inputs.channel }}
SLACK_COLOR: danger
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Failed'
SLACK_MESSAGE: ${{ env.SUBJECT }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
SLACK_FOOTER: ''
@@ -1,155 +0,0 @@
function updateOctokitInstance(github) {
updateGithubRestActions(github);
addConvenienceMethods(github);
}
function addConvenienceMethods(github) {
Object.assign(github, {
// checks if a run is a failure from the perspective of sending Slack
// notifications. Works for both runs and run attempts, which should be the
// same object shapes.
// When a run has not been completed, it fetches the jobs for the workflow
// to check if the job is "failing" instead of "failed".
async isFailedRun(run) {
console.log(`checking run: ${run.id}`);
console.log(`status: ${run.status}`);
console.log(`conclusion: ${run.conclusion}`);
console.log(`run_attempt: ${run.run_attempt}`);
if (run.status === 'completed') {
console.log('completed -- checking conclusion');
return run.conclusion === 'failure' || run.conclusion === 'timed_out';
}
const owner = run.repository.owner.login;
const repo = run.repository.name;
const jobsResp = await github.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: run.id,
});
console.log('checking jobs');
const jobs = jobsResp.data;
for (const job of jobs.jobs) {
if (job.conclusion === 'failure' || job.conclusion === 'timed_out') {
console.log('job failed');
return true;
} else if (job.status === 'completed') {
continue;
}
for (const step of job.steps) {
if (step.conclusion === 'failure') {
console.log('job step failed');
return true;
}
}
}
return false;
},
});
}
// github-script's Octokit/REST plugins are out of date, so we have to
// add our own custom endpoint method
// see https://octokit.github.io/rest.js/v18#custom-endpoint-methods for
// documentation
function updateGithubRestActions(github) {
Object.assign(github.rest.actions, {
getWorkflowRunAttempt: github.request.defaults({
method: 'GET',
url: '/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}',
params: {
owner: {
required: true,
type: 'string',
},
repo: {
required: true,
type: 'string',
},
run_id: {
required: true,
type: 'number',
},
attempt_number: {
required: true,
type: 'number',
},
},
}),
});
}
// Returns { current: 'failure' } | { current: 'success', previous: 'success' | 'failure' } to indicate the failure status
// of the current and previous runs
module.exports = async ({ github }) => {
const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
const [owner, repo] = GITHUB_REPOSITORY.split('/');
updateOctokitInstance(github);
const workflowRunResp = await github.rest.actions.getWorkflowRun({
owner,
repo,
run_id: GITHUB_RUN_ID,
});
const currentRun = workflowRunResp.data;
const listWorkflowRunsResp = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: currentRun.workflow_id,
branch: GITHUB_REF_NAME,
});
const { workflow_runs } = listWorkflowRunsResp.data;
if (workflow_runs.length === 1 && workflow_runs[0].run_attempt === 1) {
return false;
}
const previousRuns = workflow_runs.filter(
(run) => run.status === 'completed' && run.conclusion !== 'cancelled'
);
if (await github.isFailedRun(currentRun)) {
return { current: 'failure', previous: null };
}
if (currentRun.run_attempt === 1 && previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
for (let i = currentRun.run_attempt - 1; i > 0; i--) {
const previousAttemptResp = await github.rest.actions.getWorkflowRunAttempt({
owner,
repo,
run_id: currentRun.id,
attempt_number: i,
});
const previousAttempt = previousAttemptResp.data;
if (
previousAttempt.conclusion === 'success' ||
previousAttempt.conclusion === 'failure' ||
previousAttempt.conclusion === 'timed_out'
) {
return {
current: 'success',
previous: (await github.isFailedRun(previousAttempt)) ? 'failure' : 'success',
};
}
}
if (previousRuns.length > 0) {
return {
current: 'success',
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
};
}
return { current: 'success', previous: 'success' };
};
@@ -1,12 +0,0 @@
{
"name": "notify-slack-on-fail-or-recover",
"version": "1.0.0",
"description": "A GitHub Action to notify Expo Slack of build failures and recovery.",
"main": "main.js",
"author": "Expo",
"license": "UNLICENSED",
"private": true,
"devDependencies": {
"prettier": "^2.5.1"
}
}
@@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
[email protected]:
version "2.5.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
+1 -3
View File
@@ -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 ') }} if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '16'
- name: Checkout Pull Request - name: Checkout Pull Request
run: hub pr checkout ${{ github.event.issue.number }} run: hub pr checkout ${{ github.event.issue.number }}
env: env:
+20
View File
@@ -0,0 +1,20 @@
name: Installer tests
on:
push:
branches: [main]
pull_request:
types: [opened, synchronize]
jobs:
test:
name: Run the installer
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v2
- run: bash bin/install.sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: eas --version
-146
View File
@@ -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'
})
+12 -22
View File
@@ -27,7 +27,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '16'
- run: yarn install --frozen-lockfile --check-files - run: yarn install --frozen-lockfile --check-files
- run: yarn build - run: yarn build
- name: Build tarballs - name: Build tarballs
@@ -36,10 +36,10 @@ jobs:
yarn pretarball-ci yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
- id: x64 - id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- id: arm - id: arm
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload linux-x64 tarball - name: Upload linux-x64 tarball
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -68,7 +68,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '16'
- run: yarn install --frozen-lockfile --check-files - run: yarn install --frozen-lockfile --check-files
- run: yarn build - run: yarn build
- name: Build tarballs - name: Build tarballs
@@ -77,7 +77,7 @@ jobs:
yarn pretarball-ci yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64 yarn oclif pack:tarballs --no-xz --targets darwin-x64
- id: x64 - id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist working-directory: ${{ github.workspace }}/packages/eas-cli/dist
- name: Upload darwin-x64 tarball - name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -96,7 +96,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '16'
- run: yarn install --frozen-lockfile --check-files - run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis - run: sudo apt-get install nsis
- run: yarn build - run: yarn build
@@ -105,10 +105,10 @@ jobs:
yarn oclif pack:win yarn oclif pack:win
working-directory: ${{ github.workspace }}/packages/eas-cli working-directory: ${{ github.workspace }}/packages/eas-cli
- id: x64 - id: x64
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32 working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- id: x86 - id: x86
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32 working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
- name: Upload x64 installer - name: Upload x64 installer
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
@@ -138,7 +138,7 @@ jobs:
with: with:
registry-url: 'https://registry.npmjs.org/' registry-url: 'https://registry.npmjs.org/'
scope: 'expo' scope: 'expo'
node-version: 18 node-version: '16'
- name: Install dependencies - name: Install dependencies
run: yarn install --frozen-lockfile --check-files run: yarn install --frozen-lockfile --check-files
- name: Build - name: Build
@@ -147,25 +147,15 @@ jobs:
run: yarn lerna publish from-package --yes --no-verify-access run: yarn lerna publish from-package --yes --no-verify-access
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} 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: release-changelog:
name: Update changelog and publish release name: Update changelog and publish release
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: publish-to-npm needs: publish-to-npm
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: '16'
- name: Install dependencies - name: Install dependencies
working-directory: ./scripts working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files run: yarn install --frozen-lockfile --check-files
@@ -199,9 +189,9 @@ jobs:
env: env:
SLACK_CHANNEL: eas-cli SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }} SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-bot.png?size=48 SLACK_ICON: https://github.com/expo-ci.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}' SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }} SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'EAS CLI CI' SLACK_USERNAME: 'Expo CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: '' SLACK_FOOTER: ''
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 16
- uses: preactjs/compressed-size-action@v2 - uses: preactjs/compressed-size-action@v2
with: with:
build-script: 'build:tarballs:linux' build-script: 'build:tarballs:linux'
-26
View File
@@ -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
+3 -21
View File
@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['14', '16', '18'] node: ['12', '14', '16']
include: include:
- node: '18' - node: '16'
coverage: true coverage: true
name: Test with Node ${{ matrix.node }} name: Test with Node ${{ matrix.node }}
steps: steps:
@@ -27,27 +27,9 @@ jobs:
if: ${{ !matrix.coverage }} if: ${{ !matrix.coverage }}
- run: yarn test --coverage - run: yarn test --coverage
if: ${{ matrix.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 lint --max-warnings=0
- run: yarn install --frozen-lockfile --check-files - run: shellcheck bin/*.sh
working-directory: ./scripts
- run: yarn lint-changelog
- uses: codecov/codecov-action@v1 - uses: codecov/codecov-action@v1
if: ${{ matrix.coverage }} if: ${{ matrix.coverage }}
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
notify-slack:
runs-on: ubuntu-latest
needs: test
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
- uses: actions/checkout@v2
- name: Notify Slack
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
with:
channel: eas-cli
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
-2
View File
@@ -2,12 +2,10 @@
packages/*/build packages/*/build
packages/eas-cli/dist packages/eas-cli/dist
packages/eas-cli/tmp packages/eas-cli/tmp
scripts/build
# Code editors # Code editors
.idea .idea
.history .history
.vscode
# macOS # macOS
.DS_Store .DS_Store
+1 -2
View File
@@ -4,6 +4,5 @@
"singleQuote": true, "singleQuote": true,
"bracketSameLine": true, "bracketSameLine": true,
"trailingComma": "es5", "trailingComma": "es5",
"arrowParens": "avoid", "arrowParens": "avoid"
"endOfLine": "auto"
} }
+23 -687
View File
@@ -12,670 +12,6 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores ### 🧹 Chores
## [3.9.3](https://github.com/expo/eas-cli/releases/tag/v3.9.3) - 2023-04-18
### 🐛 Bug fixes
- Fix prompts in `eas update`. ([#1797](https://github.com/expo/eas-cli/pull/1797) by [@wschurman](https://github.com/wschurman))
## [3.9.2](https://github.com/expo/eas-cli/releases/tag/v3.9.2) - 2023-04-13
### 🐛 Bug fixes
- Fixes the rollout percentages when ending a rollout. ([#1781](https://github.com/expo/eas-cli/pull/1781) by [@jonsamp](https://github.com/jonsamp))
- Print meaningful error message if `extra.eas.projectId` is not a string. ([#1788](https://github.com/expo/eas-cli/pull/1788) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Show output of `expo install expo-updates`. ([#1782](https://github.com/expo/eas-cli/pull/1782) by [@wkozyra95](https://github.com/wkozyra95))
- Print deprecation warnings for deprecated `eas.json` fields. ([#1768](https://github.com/expo/eas-cli/pull/1768) by [@szdziedzic](https://github.com/szdziedzic))
## [3.9.1](https://github.com/expo/eas-cli/releases/tag/v3.9.1) - 2023-04-10
### 🐛 Bug fixes
- Update apple utilities to work around the maintenance error. ([#1780](https://github.com/expo/eas-cli/pull/1780) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Don't use defaults for the `cache.cacheDefaultPaths` build profile field. ([#1769](https://github.com/expo/eas-cli/pull/1769) by [@szdziedzic](https://github.com/szdziedzic))
## [3.9.0](https://github.com/expo/eas-cli/releases/tag/v3.9.0) - 2023-04-06
### 🎉 New features
- Add support for roll back to embedded updates. ([#1754](https://github.com/expo/eas-cli/pull/1754), [#1755](https://github.com/expo/eas-cli/pull/1755) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Update `expoCommandAsync` to support the new Expo CLI bin path. ([#1772](https://github.com/expo/eas-cli/pull/1772) by [@gabrieldonadel](https://github.com/gabrieldonadel))
### 🧹 Chores
- Use the `eas-cli` npm tag for checking for the local build plugin updates. ([#1759](https://github.com/expo/eas-cli/pull/1759) by [@dsokal](https://github.com/dsokal))
- Ignore `requireCommit` option in `eas build:internal`. ([#1760](https://github.com/expo/eas-cli/pull/1760) by [@wkozyra95](https://github.com/wkozyra95))
- Support parsing eas.json from a string. ([#1766](https://github.com/expo/eas-cli/pull/1766) by [@wkozyra95](https://github.com/wkozyra95))
## [3.8.1](https://github.com/expo/eas-cli/releases/tag/v3.8.1) - 2023-03-16
### 🐛 Bug fixes
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
### 🎉 New features
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
### 🐛 Bug fixes
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
### 🐛 Bug fixes
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
### 🎉 New features
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
### 🐛 Bug fixes
- Disable analytics when running behind a proxy. ([#1696](https://github.com/expo/eas-cli/pull/1696) by [@wkozyra95](https://github.com/wkozyra95))
- Clarify missing owner field error message for Robot users. ([#1702](https://github.com/expo/eas-cli/pull/1702) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Validate the platform for local builds earlier. ([#1698](https://github.com/expo/eas-cli/pull/1698) by [@wkozyra95](https://github.com/wkozyra95))
- Display build message when picking build in `eas submit`, `eas build:resign`, and `eas build:run` commands. ([#1700](https://github.com/expo/eas-cli/pull/1700) by [@szdziedzic](https://github.com/szdziedzic))
- Add a link directly to a build phase logs in the EAS CLI build error message. ([#1699](https://github.com/expo/eas-cli/pull/1699) by [@szdziedzic](https://github.com/szdziedzic))
## [3.6.0](https://github.com/expo/eas-cli/releases/tag/v3.6.0) - 2023-02-14
### 🎉 New features
- Use `sdkVersion` as default runtime version policy when running `eas update:configure`. ([#1669](https://github.com/expo/eas-cli/pull/1669) by [@jonsamp](https://github.com/jonsamp))
- Warn when project ID but no owner specified and mismatch logged in user. ([#1667](https://github.com/expo/eas-cli/pull/1667) by [@wschurman](https://github.com/wschurman))
- Return the build `message` if one is available. ([#1691](https://github.com/expo/eas-cli/pull/1691) by [@raulriera](https://github.com/raulriera))
### 🐛 Bug fixes
- Fix suggested application identifier to match owning account name. ([#1670](https://github.com/expo/eas-cli/pull/1670) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Add Xcode 14.2 image to the VSCode schema. ([#1693](https://github.com/expo/eas-cli/pull/1693) by [@szdziedzic](https://github.com/szdziedzic))
## [3.5.2](https://github.com/expo/eas-cli/releases/tag/v3.5.2) - 2023-02-05
### 🐛 Bug fixes
- Use the new developer-mdn.apple.com subdomain instead of developer.apple.com. ([#1673](https://github.com/expo/eas-cli/pull/1673) by [@brentvatne](https://github.com/brentvatne))
## [3.5.1](https://github.com/expo/eas-cli/releases/tag/v3.5.1) - 2023-02-01
### 🐛 Bug fixes
- Fix the issue with the `eas.json` envs being not available when resolving dynamic config. ([#1666](https://github.com/expo/eas-cli/pull/1666) by [@szdziedzic](https://github.com/szdziedzic))
## [3.5.0](https://github.com/expo/eas-cli/releases/tag/v3.5.0) - 2023-01-31
### 🎉 New features
- Set `m1-medium` resource class for SDK version `>=48` and RN version `>=0.71` builds with unspecified resource class. ([#1655](https://github.com/expo/eas-cli/pull/1655) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Fix undefined branchMappingLogic. ([#1653](https://github.com/expo/eas-cli/pull/1653) by [@quinlanj](https://github.com/quinlanj))
- Fix using local credentials for internal distribution builds with universal provisioning. ([#1657](https://github.com/expo/eas-cli/pull/1657) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Move image validation to the server side and better handle server validation errors in the eas-cli. ([#1650](https://github.com/expo/eas-cli/pull/1650) by [@szdziedzic](https://github.com/szdziedzic))
- Update metadata on the worker when using git-based builds. ([#1651](https://github.com/expo/eas-cli/pull/1651) by [@wkozyra95](https://github.com/wkozyra95))
## [3.4.1](https://github.com/expo/eas-cli/releases/tag/v3.4.1) - 2023-01-23
### 🐛 Bug fixes
- Fix incorrect exit code for successful local builds. ([#1649](https://github.com/expo/eas-cli/pull/1649) by [@dsokal](https://github.com/dsokal))
## [3.4.0](https://github.com/expo/eas-cli/releases/tag/v3.4.0) - 2023-01-20
### 🎉 New features
- Add `--json` flag to `eas config` command. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
- Add M1 resource class configuration to default eas.json when running eas build:configure on a new project. ([#1637](https://github.com/expo/eas-cli/pull/1637) by [@brentvatne](https://github.com/brentvatne))
- Add prompt to switch iOS builds to M1 if the build queue is long. ([#1642](https://github.com/expo/eas-cli/pull/1642) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix running commands with `--json` flag when EAS_NO_VCS is set. ([#1641](https://github.com/expo/eas-cli/pull/1641) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Remove unnecessary workaround for trailing backslash in .gitignore. ([#1622](https://github.com/expo/eas-cli/pull/1622) by [@wkozyra95](https://github.com/wkozyra95))
- Internal command that will be run on EAS worker when building from GitHub. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
- Make the disabled tier error message more descriptive. ([#1635](https://github.com/expo/eas-cli/pull/1635) by [@dsokal](https://github.com/dsokal))
- Control some of the EAS Build error messages server-side. ([#1639](https://github.com/expo/eas-cli/pull/1639) by [@wkozyra95](https://github.com/wkozyra95))
## [3.3.2](https://github.com/expo/eas-cli/releases/tag/v3.3.2) - 2023-01-12
### 🐛 Bug fixes
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1619](https://github.com/expo/eas-cli/pull/1619) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Upgrade dependencies. ([#1617](https://github.com/expo/eas-cli/pull/1617) by [@dsokal](https://github.com/dsokal))
- Improve handling `SSOUser` type. ([#1621](https://github.com/expo/eas-cli/pull/1621) by [@szdziedzic](https://github.com/szdziedzic))
## [3.3.1](https://github.com/expo/eas-cli/releases/tag/v3.3.1) - 2023-01-11
### 🐛 Bug fixes
- Revert missing key information about updates in `eas update --json` and `eas update:view --json`. ([d20db0f](https://github.com/expo/eas-cli/commit/d20db0fff42a6f4512fea426a66cb6f168e37f7c) by [@ide](https://github.com/ide))
## [3.3.0](https://github.com/expo/eas-cli/releases/tag/v3.3.0) - 2023-01-11
### 🎉 New features
- Add ability to select resource class for a build in build profile. ([#1609](https://github.com/expo/eas-cli/pull/1609) by [@dsokal](https://github.com/dsokal))
- Deprecate `--resource-class` flag. ([#1615](https://github.com/expo/eas-cli/pull/1615) by [@dsokal](https://github.com/dsokal))
- Add new resource classes and update already available. ([#1616](https://github.com/expo/eas-cli/pull/1616) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1611](https://github.com/expo/eas-cli/pull/1611) by [@byCedric](https://github.com/byCedric))
## [3.2.1](https://github.com/expo/eas-cli/releases/tag/v3.2.1) - 2023-01-09
### 🐛 Bug fixes
- Validate chosen build in the `eas build:run` command. ([#1614](https://github.com/expo/eas-cli/pull/1614) by [@szdziedzic](https://github.com/szdziedzic))
## [3.2.0](https://github.com/expo/eas-cli/releases/tag/v3.2.0) - 2023-01-09
### 🎉 New features
- Add `--channel` option to `update:republish`. ([#1580](https://github.com/expo/eas-cli/pull/1580) by [@byCedric](https://github.com/byCedric))
- Use `appVersion` as default runtime version policy when running `eas update:configure`. ([#1588](https://github.com/expo/eas-cli/pull/1588) by [@jonsamp](https://github.com/jonsamp))
- Support `--json` flag in webhook list command. ([#1605](https://github.com/expo/eas-cli/pull/1605) by [@sheddy7](https://github.com/sheddy7))
### 🐛 Bug fixes
- Makes eas.json configuration to only run on `update:configure`. ([#1598](https://github.com/expo/eas-cli/pull/1598) by [@jonsamp](https://github.com/jonsamp))
- Fix issue with invisible build info in some terminals in the `eas build:run` and `eas build:resign` commands. ([#1602](https://github.com/expo/eas-cli/pull/1602) by [@szdziedzic](https://github.com/szdziedzic))
- Fix issues with invisible build info in some terminals while using the `eas submit` command. ([#1603](https://github.com/expo/eas-cli/pull/1603) by [@szdziedzic](https://github.com/szdziedzic))
- Use the `completedAt` timestamp as the build finish date instead of the `updatedAt` timestamp in the `eas build:run` command. ([#1604](https://github.com/expo/eas-cli/pull/1604) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Make all URLs in logs clickable in terminals supporting hyperlinks. ([#1591](https://github.com/expo/eas-cli/pull/1591) by [@Simek](https://github.com/Simek))
- Use paginated select prompt for the `eas build:run` command. ([#1601](https://github.com/expo/eas-cli/pull/1601) by [@szdziedzic](https://github.com/szdziedzic))
## [3.1.1](https://github.com/expo/eas-cli/releases/tag/v3.1.1) - 2022-12-19
### 🐛 Bug fixes
- Fix update `--channel` and `--branch` flag validation. ([#1579](https://github.com/expo/eas-cli/pull/1579) by [@byCedric](https://github.com/byCedric))
## [3.1.0](https://github.com/expo/eas-cli/releases/tag/v3.1.0) - 2022-12-17
### 🎉 New features
- Adds `--channel` flag to `eas update`. ([#1567](https://github.com/expo/eas-cli/pull/1567) by [@jonsamp](https://github.com/jonsamp))
- Add channel configurations to **eas.json** during `eas update:configure`. ([#1570](https://github.com/expo/eas-cli/pull/1570) by [@jonsamp](https://github.com/jonsamp))
- Add `build:resign` command. ([#1575](https://github.com/expo/eas-cli/pull/1575) by [@wkozyra95](https://github.com/wkozyra95))
## [3.0.0](https://github.com/expo/eas-cli/releases/tag/v3.0.0) - 2022-12-07
### 🛠 Breaking changes
- Move update republish to separate command. ([#1533](https://github.com/expo/eas-cli/pull/1533) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Make the not recognized image error handler run only if the image is present under a valid key and has an invalid value. ([#1565](https://github.com/expo/eas-cli/pull/1565) by [@szdziedzic](https://github.com/szdziedzic))
- Fix the `node:assert` import error by using imports from `assert` instead of from `node:assert`. ([#1569](https://github.com/expo/eas-cli/pull/1569) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Remove old republish code from the update command. ([#1535](https://github.com/expo/eas-cli/pull/1535) by [@byCedric](https://github.com/byCedric))
## [2.9.0](https://github.com/expo/eas-cli/releases/tag/v2.9.0) - 2022-12-05
### 🎉 New features
- Add caching for `eas build:run` command. ([#1542](https://github.com/expo/eas-cli/pull/1542) by [@szdziedzic](https://github.com/szdziedzic))
- Add `isGitWorkingTreeDirty` to EAS Update records. ([#1550](https://github.com/expo/eas-cli/pull/1550) by [@FiberJW](https://github.com/FiberJW))
- Prompt developer to download the app after the simulator build is finished. ([#1554](https://github.com/expo/eas-cli/pull/1554) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Ask for the missing application identifier consistently in `eas build:version:sync` command. ([#1543](https://github.com/expo/eas-cli/pull/1543) by [@wkozyra95](https://github.com/wkozyra95))
- Disable selecting builds whose artifacts have expired in the `eas build:run` command. ([#1547](https://github.com/expo/eas-cli/pull/1547) by [@szdziedzic](https://github.com/szdziedzic))
- Change intent with which the `eas build:run` command opens an Android app to `android.intent.action.MAIN`. ([#1556](https://github.com/expo/eas-cli/pull/1556) by [@szdziedzic](https://github.com/szdziedzic))
## [2.8.0](https://github.com/expo/eas-cli/releases/tag/v2.8.0) - 2022-11-28
### 🎉 New features
- Add `eas build:run` support for Android. ([#1485](https://github.com/expo/eas-cli/pull/1485) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Only create a new channel when update branch is new. ([#1507](https://github.com/expo/eas-cli/pull/1507) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Better wording in eas submit. ([#1527](https://github.com/expo/eas-cli/pull/1527) by [@dsokal](https://github.com/dsokal))
- Upload project sources for EAS Build and archives for EAS Submit to GCS. ([#1524](https://github.com/expo/eas-cli/pull/1524) by [@wkozyra95](https://github.com/wkozyra95))
- Add a better error message for when an invalid `image` is set in `eas-json`. ([#1531](https://github.com/expo/eas-cli/pull/1531) by [@szdziedzic](https://github.com/szdziedzic))
- Remove internal expo id from device formatter. ([#1541](https://github.com/expo/eas-cli/pull/1541) by [@dsokal](https://github.com/dsokal))
## [2.7.1](https://github.com/expo/eas-cli/releases/tag/v2.7.1) - 2022-11-16
### 🐛 Bug fixes
- Use envs from the build profile to resolve credentials in `eas credentials`. ([#1520](https://github.com/expo/eas-cli/pull/1520) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Print prompt message when failing because of the non-interactive shell. ([#1523](https://github.com/expo/eas-cli/pull/1523) by [@wkozyra95](https://github.com/wkozyra95))
- Add SDK version to the analytics build events. ([#1529](https://github.com/expo/eas-cli/pull/1529) by [@wkozyra95](https://github.com/wkozyra95))
- Add Xcode 14.1 image to `eas-json`. ([#1511](https://github.com/expo/eas-cli/pull/1511) by [@szdziedzic](https://github.com/szdziedzic))
## [2.7.0](https://github.com/expo/eas-cli/releases/tag/v2.7.0) - 2022-11-11
### 🎉 New features
- Support uploading different platform combinations in `eas update`. ([#1461](https://github.com/expo/eas-cli/pull/1461) by [@EvanBacon](https://github.com/EvanBacon))
- Add Git commit to EAS Update record. ([#1499](https://github.com/expo/eas-cli/pull/1499) by [@fiberjw](https://github.com/fiberjw))
- Use local CLI for `expo export` in SDK +46. ([#1474](https://github.com/expo/eas-cli/pull/1474) by [@EvanBacon](https://github.com/EvanBacon))
- Validate icon PNGs before running Android build. ([#1477](https://github.com/expo/eas-cli/pull/1477) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fixed the values of the distribution enum on the build profile schema receiving the incorrect descriptions. ([#1504](https://github.com/expo/eas-cli/pull/1504) by [@macksal](https://github.com/macksal))
- Remove hard dependency on expo package being installed to init a project. ([#1517](https://github.com/expo/eas-cli/pull/1517) by [@brentvatne](https://github.com/brentvatne))
### 🧹 Chores
- Remove friction from the initial EAS update setup. ([#1479](https://github.com/expo/eas-cli/pull/1479) by [@byCedric](https://github.com/byCedric))
- Improve choice formatting when selecting a build to submit in the `eas submit` command. ([#1502](https://github.com/expo/eas-cli/pull/1502) by [@szdziedzic](https://github.com/szdziedzic))
## [2.6.0](https://github.com/expo/eas-cli/releases/tag/v2.6.0) - 2022-10-27
### 🎉 New features
- Add custom metadata validation command with more complex rules. ([#1416](https://github.com/expo/eas-cli/pull/1416) by [@byCedric](https://github.com/byCedric))
- Add `eas build:run` command which runs iOS simulator builds from the CLI. ([#1447](https://github.com/expo/eas-cli/pull/1447) by [@szdziedzic](https://github.com/szdziedzic))
- Add feature gate support. ([#1475](https://github.com/expo/eas-cli/pull/1475) by [@wschurman](https://github.com/wschurman))
- Warn about outdated build deployment when configuring EAS Update. ([#1467](https://github.com/expo/eas-cli/pull/1467) by [@fiberjw](https://github.com/fiberjw))
### 🧹 Chores
- Replace `secret:list` table with formatted fields. ([#1464](https://github.com/expo/eas-cli/pull/1464) by [@byCedric](https://github.com/byCedric))
- Replace `device:create` input table with formatted fields. ([#1465](https://github.com/expo/eas-cli/pull/1465) by [@byCedric](https://github.com/byCedric))
- Replace `channel:view` table with formatted fields. ([#1466](https://github.com/expo/eas-cli/pull/1466) by [@byCedric](https://github.com/byCedric))
- Upgrade dependencies. ([#1480](https://github.com/expo/eas-cli/pull/1480) by [@dsokal](https://github.com/dsokal))
- Replace all remaining tables with formatted fields. ([#1481](https://github.com/expo/eas-cli/pull/1481) by [@byCedric](https://github.com/byCedric))
## [2.5.1](https://github.com/expo/eas-cli/releases/tag/v2.5.1) - 2022-10-24
### 🐛 Bug fixes
- Revert using local CLI for `expo export` ([#1460](https://github.com/expo/eas-cli/pull/1460)). ([#1472](https://github.com/expo/eas-cli/pull/1472) by [@dsokal](https://github.com/dsokal))
## [2.5.0](https://github.com/expo/eas-cli/releases/tag/v2.5.0) - 2022-10-24
### 🎉 New features
- Update init command to handle slug and owner. ([#1452](https://github.com/expo/eas-cli/pull/1452) by [@wschurman](https://github.com/wschurman))
- Add `eas secret:push`. ([#1457](https://github.com/expo/eas-cli/pull/1457) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Initialize analytics consistently and move analytics into context. ([#1444](https://github.com/expo/eas-cli/pull/1444) by [@wschurman](https://github.com/wschurman))
- - Skip using `--non-interactive` and `--experimental-bundle` flags when local Expo CLI is installed. ([#1460](https://github.com/expo/eas-cli/pull/1460) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Handle case where git is installed but not working properly. ([#1454](https://github.com/expo/eas-cli/pull/1454) by [@brentvatne](https://github.com/brentvatne))
- Remove table logging in `update:view`. ([#1463](https://github.com/expo/eas-cli/pull/1463) by [@EvanBacon](https://github.com/EvanBacon))
- Fix install prompt language. ([#1462](https://github.com/expo/eas-cli/pull/1462) by [@EvanBacon](https://github.com/EvanBacon))
## [2.4.1](https://github.com/expo/eas-cli/releases/tag/v2.4.1) - 2022-10-17
### 🐛 Bug fixes
- Bump `eas-cli-local-build-plugin` to 0.0.144 to not require `yarn` for local builds. ([c05ef](https://github.com/expo/eas-cli/commit/c05ef9cccaa819d8e5fb84858cb09cadaae90134) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Remove EAS Submit errors from codebase. The errors have been moved to the server side. ([#1443](https://github.com/expo/eas-cli/pull/1443) by [@dsokal](https://github.com/dsokal))
## [2.4.0](https://github.com/expo/eas-cli/releases/tag/v2.4.0) - 2022-10-14
### 🎉 New features
- Handle more common errors for EAS Submit. ([#1440](https://github.com/expo/eas-cli/pull/1440) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Move graphql and session into command context. ([#1435](https://github.com/expo/eas-cli/pull/1435), [#1436](https://github.com/expo/eas-cli/pull/1436) by [@wschurman](https://github.com/wschurman))
## [2.3.0](https://github.com/expo/eas-cli/releases/tag/v2.3.0) - 2022-10-10
### 🎉 New features
- Add new experimental resource class for M1 Macs. ([#1425](https://github.com/expo/eas-cli/pull/1425) by [@szdziedzic](https://github.com/szdziedzic))
## [2.2.1](https://github.com/expo/eas-cli/releases/tag/v2.2.1) - 2022-10-05
### 🐛 Bug fixes
- Make Apple Platform detection more robust. ([#1417](https://github.com/expo/eas-cli/pull/1417) by [@quinlanj](https://github.com/quinlanj))
- Fix creating EAS secret from file. ([#1423](https://github.com/expo/eas-cli/pull/1423) by [@dsokal](https://github.com/dsokal))
## [2.2.0](https://github.com/expo/eas-cli/releases/tag/v2.2.0) - 2022-10-04
### 🛠 Breaking changes
- Prompt before configuring EAS project. ([#1356](https://github.com/expo/eas-cli/pull/1356) by [@wschurman](https://github.com/wschurman))
- Adds paginated support to device commands. Removes the ability to delete multiple devices at once. ([#1381](https://github.com/expo/eas-cli/pull/1381) by [@kgc00](https://github.com/kgc00))
### 🎉 New features
- Add `-e` as a shortcut for `--profile` flag. ([#1342](https://github.com/expo/eas-cli/pull/1342) by [@szdziedzic](https://github.com/szdziedzic))
- Support JSON5 in **eas.json**. ([#1350](https://github.com/expo/eas-cli/pull/1350) by [@dsokal](https://github.com/dsokal))
- Add pagination and interactivity to update commands. ([#1323](https://github.com/expo/eas-cli/pull/1323) by [@kgc00](https://github.com/kgc00))
- Add support for uploading files to EAS Secret. ([#1354](https://github.com/expo/eas-cli/pull/1354) by [@dsokal](https://github.com/dsokal))
- Add pagination to channel commands. ([#1352](https://github.com/expo/eas-cli/pull/1352) by [@kgc00](https://github.com/kgc00))
- Add pagination to build commands. ([#1353](https://github.com/expo/eas-cli/pull/1353) by [@kgc00](https://github.com/kgc00))
- Provide suggestions for developers when archive size is large. ([#1363](https://github.com/expo/eas-cli/pull/1363) by [@szdziedzic](https://github.com/szdziedzic))
- Improve `eas init` command. ([#1376](https://github.com/expo/eas-cli/pull/1376) by [@wschurman](https://github.com/wschurman))
- Warn about outdated channel configuration before build when using eas update. ([#1397](https://github.com/expo/eas-cli/pull/1397) by [@kbrandwijk](https://github.com/kbrandwijk))
- Validate that owner and projectId and slug all are in alignment. ([#1405](https://github.com/expo/eas-cli/pull/1405) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Fix automatic eas.json generation when running `eas build`. ([#1415](https://github.com/expo/eas-cli/pull/1415) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Standardize the GQL 'Update' schema using UpdateFragmentNode. ([#1348](https://github.com/expo/eas-cli/pull/1348) by [@kgc00](https://github.com/kgc00))
- Remove 'owner' app config field dependency. ([#1368](https://github.com/expo/eas-cli/pull/1368), [#1369](https://github.com/expo/eas-cli/pull/1369), [#1371](https://github.com/expo/eas-cli/pull/1371), [#1372](https://github.com/expo/eas-cli/pull/1372), [#1373](https://github.com/expo/eas-cli/pull/1373) by [@wschurman](https://github.com/wschurman))
- Add Xcode 14 image. ([#1365](https://github.com/expo/eas-cli/pull/1365) by [@szdziedzic](https://github.com/szdziedzic))
- Update max `versionCode` for Android. ([#1400](https://github.com/expo/eas-cli/pull/1400) by [@wkozyra95](https://github.com/wkozyra95))
- Add and use command context to declare command dependencies. ([#1383](https://github.com/expo/eas-cli/pull/1383), [#1384](https://github.com/expo/eas-cli/pull/1384), [#1387](https://github.com/expo/eas-cli/pull/1387), [#1388](https://github.com/expo/eas-cli/pull/1388), [#1390](https://github.com/expo/eas-cli/pull/1390), [#1391](https://github.com/expo/eas-cli/pull/1391), [#1394](https://github.com/expo/eas-cli/pull/1394), [#1402](https://github.com/expo/eas-cli/pull/1402), [#1403](https://github.com/expo/eas-cli/pull/1403) by [@wschurman](https://github.com/wschurman))
- Fix typo in the ad hoc build message. ([#1407](https://github.com/expo/eas-cli/pull/1407) by [@Simek](https://github.com/Simek))
- Improve errors and error messages formatting related to **eas.json**. ([#1414](https://github.com/expo/eas-cli/pull/1414) by [@Simek](https://github.com/Simek))
- Handle errors from platform-specific kill switches to disable free tier builds. ([#1401](https://github.com/expo/eas-cli/pull/1401) by [@szdziedzic](https://github.com/szdziedzic))
- Surface invalid eas.json errors in an optional project context. ([#1418](https://github.com/expo/eas-cli/pull/1418) by [@quinlanj](https://github.com/quinlanj))
- Improve message when project ID doesn't match either slug or owner. ([#1420](https://github.com/expo/eas-cli/pull/1420) by [@brentvatne](https://github.com/brentvatne))
## [2.1.0](https://github.com/expo/eas-cli/releases/tag/v2.1.0) - 2022-09-05
### 🎉 New features
- Add tvOS credentials compatibility for Adhoc and App store builds. ([#1325](https://github.com/expo/eas-cli/pull/1325) by [@quinlanj](https://github.com/quinlanj))
- Add `eas open` command for opening project page in web browser. ([#1337](https://github.com/expo/eas-cli/pull/1337) by [@dsokal](https://github.com/dsokal))
- Add support for buildArtifactPaths. Rename artifactPath to applicationArchivePath. ([#1321](https://github.com/expo/eas-cli/pull/1321) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix viewing branch list when a branch has no associated platforms. ([#1326](https://github.com/expo/eas-cli/pull/1326) by [@hbiede](https://github.com/hbiede))
- Fix description of `help` command in help prompt. ([#1341](https://github.com/expo/eas-cli/pull/1341) by [@Simek](https://github.com/Simek))
- Resolve paths from the root when checking for gitignored values. ([#1336](https://github.com/expo/eas-cli/pull/1336) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add noImplicitOverride to tsconfigs. ([#1327](https://github.com/expo/eas-cli/pull/1327) by [@wschurman](https://github.com/wschurman))
- Fix typos. ([#1340](https://github.com/expo/eas-cli/pull/1340) by [@hbiede](https://github.com/hbiede))
## [2.0.0](https://github.com/expo/eas-cli/releases/tag/v2.0.0) - 2022-09-01
### 🛠 Breaking changes
- Fix typo in `cli.promptToConfigurePushNotifications` field in **eas.json**. EAS CLI will throw an error if the old field name is in **eas.json**. ([#1332](https://github.com/expo/eas-cli/pull/1332) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Fix dynamic config update warning. ([#1322](https://github.com/expo/eas-cli/pull/1322) by [@wschurman](https://github.com/wschurman))
## [1.2.0](https://github.com/expo/eas-cli/releases/tag/v1.2.0) - 2022-08-29
### 🎉 New features
- Add 2022 Apple capabilities. ([#1307](https://github.com/expo/eas-cli/pull/1307) by [@EvanBacon](https://github.com/EvanBacon))
- Print warning when eas-cli is installed as project dependency. ([#1310](https://github.com/expo/eas-cli/pull/1310) by [@dsokal](https://github.com/dsokal))
- Add eas.json to skip push notifications credentials setup. ([#1315](https://github.com/expo/eas-cli/pull/1315) by [@dsokal](https://github.com/dsokal))
- Warn about EAS outages when running `build`, `submit`, and `update` commands. ([#1312](https://github.com/expo/eas-cli/pull/1312) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Replace promptToCreateProjectIfNotExistsAsync with getProjectIdAsync. ([#1303](https://github.com/expo/eas-cli/pull/1303) by [@wschurman](https://github.com/wschurman))
- Use AppQuery instead of ProjectQuery. ([#1304](https://github.com/expo/eas-cli/pull/1304) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Improve instructions on setting `extra.eas.projectId` in app configuration. ([#1316](https://github.com/expo/eas-cli/pull/1316) by [@dsokal](https://github.com/dsokal))
- Improve copy in EAS Submit - "e-mail" -> email, and make the link to app on App Store go directly to the TestFlight tab. ([#1318](https://github.com/expo/eas-cli/pull/1318) by [@brentvatne](https://github.com/brentvatne))
## [1.1.1](https://github.com/expo/eas-cli/releases/tag/v1.1.1) - 2022-08-23
### 🎉 New features
- Add support for `autoIncrement` option at the root of the build profile. ([#1298](https://github.com/expo/eas-cli/pull/1298) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
- Handle trailing backslash in `.gitignore`. ([#1306](https://github.com/expo/eas-cli/pull/1306) by [@wkozyra95](https://github.com/wkozyra95))
- Retry ASC Api Key downloads if it has not fully propagated on Apple's infrastructure. ([#1302](https://github.com/expo/eas-cli/pull/1302) by [@quinlanj](https://github.com/quinlanj))
## [1.1.0](https://github.com/expo/eas-cli/releases/tag/v1.1.0) - 2022-08-23
### 🎉 New features
- Add support for passing platform flag to `credentials` command. ([#1277](https://github.com/expo/eas-cli/pull/1277) by [@Simek](https://github.com/Simek))
### 🐛 Bug fixes
- Prevent throwing dynamic app config write error when configuring project ID. ([#1301](https://github.com/expo/eas-cli/pull/1301) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unused dependency and devDependency from the CLI. ([#1297](https://github.com/expo/eas-cli/pull/1297) by [@Simek](https://github.com/Simek))
## [1.0.0](https://github.com/expo/eas-cli/releases/tag/v1.0.0) - 2022-08-22
### 🛠 Breaking changes
- Remove timeout when waiting for build / submission. ([#1289](https://github.com/expo/eas-cli/pull/1289) by [@dsokal](https://github.com/dsokal))
### 🎉 New features
- Add metadata support for dynamic store.config.js files. ([#1270](https://github.com/expo/eas-cli/pull/1270) by [@byCedric](https://github.com/byCedric))
- Improve reliability of update asset presigned upload requests. ([#1278](https://github.com/expo/eas-cli/pull/1278) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Rebind `console.info` correctly after `ora` instance stops. ([#1113](https://github.com/expo/eas-cli/pull/1113) by [@EvanBacon](https://github.com/EvanBacon))
- Fix initializing git repository in monorepo. ([#1279](https://github.com/expo/eas-cli/pull/1279) by [@dsokal](https://github.com/dsokal))
- Limit the number of SignedAssetUploadSpecifications fetched at a time. ([#1287](https://github.com/expo/eas-cli/pull/1287) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Remove unused install script. ([#1280](https://github.com/expo/eas-cli/pull/1280), [#1281](https://github.com/expo/eas-cli/pull/1281) by [@wkozyra95](https://github.com/wkozyra95))
- Remove "please" from output ([#1250](https://github.com/expo/eas-cli/pull/1250) by [@jonsamp](https://github.com/jonsamp))
## [0.60.0](https://github.com/expo/eas-cli/releases/tag/v0.60.0) - 2022-08-12
### 🎉 New features
- Add `appVersion` runtime policy. ([#1267](https://github.com/expo/eas-cli/pull/1267) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Add JSON Schema `metadataPath` to iOS submission profile. ([#1269](https://github.com/expo/eas-cli/pull/1269) by [@byCedric](https://github.com/byCedric))
- Add warning when `appVersion` runtime policy is not supported. ([#1271](https://github.com/expo/eas-cli/pull/1271) by [@wkozyra95](https://github.com/wkozyra95))
## [0.59.0](https://github.com/expo/eas-cli/releases/tag/v0.59.0) - 2022-08-10
### 🐛 Bug fixes
- Fix building Android projects locally that don't have execute permissions set for `gradlew`. ([82231](https://github.com/expo/eas-cli/commit/822313e90e94bd6ddd3872061a0c25a8aa4db7cd) by [@dsokal](https://github.com/dsokal))
- Disable `nativeVersion` policy only for remote version source. ([#1261](https://github.com/expo/eas-cli/pull/1261) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Improve wording on version auto-increment. ([#1260](https://github.com/expo/eas-cli/pull/1260) by [@wkozyra95](https://github.com/wkozyra95))
- Upgrade dependencies. ([#1262](https://github.com/expo/eas-cli/pull/1262) by [@dsokal](https://github.com/dsokal))
## [0.58.0](https://github.com/expo/eas-cli/releases/tag/v0.58.0) - 2022-08-09
### 🎉 New features
- Managed app versions support. ([#1209](https://github.com/expo/eas-cli/pull/1209), [#1219](https://github.com/expo/eas-cli/pull/1219), [#1232](https://github.com/expo/eas-cli/pull/1232) by [@wkozyra95](https://github.com/wkozyra95))
- Add a warning when publishing an update with too many assets. ([#1243](https://github.com/expo/eas-cli/pull/1243) by [@kgc00](https://github.com/kgc00))
- Add submission info when building with `--json` option. ([#1246](https://github.com/expo/eas-cli/pull/1246) by [@wkozyra95](https://github.com/wkozyra95))
- Set all environment variables (documented in https://docs.expo.dev/build-reference/variables/) when running local build. ([#1256](https://github.com/expo/eas-cli/pull/1256) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Do not include sources in `eas-json` npm package. ([#1248](https://github.com/expo/eas-cli/pull/1248) by [@wkozyra95](https://github.com/wkozyra95))
- Swallow error when unable to track file in no commit workflow. ([#1109](https://github.com/expo/eas-cli/pull/1109) by [@brentvatne](https://github.com/brentvatne))
## [0.57.0](https://github.com/expo/eas-cli/releases/tag/v0.57.0) - 2022-08-03
### 🎉 New features
- Add new option to `eas device:create` - allow importing devices from Apple Developer Portal. ([#1236](https://github.com/expo/eas-cli/pull/1236) by [@dsokal](https://github.com/dsokal))
- Add support for attaching messages to builds. ([#1237](https://github.com/expo/eas-cli/pull/1237) by [@dsokal](https://github.com/dsokal))
- Introduce interactive pagination for `branch` commands. ([#1213](https://github.com/expo/eas-cli/pull/1213) by [@kgc00](https://github.com/kgc00))
## [0.56.0](https://github.com/expo/eas-cli/releases/tag/v0.56.0) - 2022-07-28
### 🎉 New features
- Display build queue type when waiting. ([#1217](https://github.com/expo/eas-cli/pull/1217) by [@dsokal](https://github.com/dsokal))
- Add better message on how to upgrade EAS CLI. ([#1222](https://github.com/expo/eas-cli/pull/1222) by [@jeremybarbet](https://github.com/jeremybarbet), [#1231](https://github.com/expo/eas-cli/pull/1231) by [@wkozyra95](https://github.com/wkozyra95))
- Invoke export with sourcemaps on update. ([#1228](https://github.com/expo/eas-cli/pull/1228) by [@kbrandwijk](https://github.com/kbrandwijk))
### 🐛 Bug fixes
- Show spinner instead of silently timing out during asset upload. ([#1206](https://github.com/expo/eas-cli/pull/1206) by [@wschurman](https://github.com/wschurman))
- Fix build archive S3 URLs. ([#1207](https://github.com/expo/eas-cli/pull/1207) by [@dsokal](https://github.com/dsokal))
- `autoIncrement` option on iOS will update versions of all targets that depend on an application target. ([#1219](https://github.com/expo/eas-cli/pull/1219) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores
- Increase max number of chained "extends" in build profiles. ([#1208](https://github.com/expo/eas-cli/pull/1208) by [@wkozyra95](https://github.com/wkozyra95))
- Change wording on build warning for bundle id/package name. ([#1211](https://github.com/expo/eas-cli/pull/1211) by [@kbrandwijk](https://github.com/kbrandwijk))
## [0.55.1](https://github.com/expo/eas-cli/releases/tag/v0.55.1) - 2022-07-12
### 🐛 Bug fixes
- Fix asset upload crash. ([#1205](https://github.com/expo/eas-cli/pull/1205) by [@wschurman](https://github.com/wschurman))
## [0.55.0](https://github.com/expo/eas-cli/releases/tag/v0.55.0) - 2022-07-11
### 🎉 New features
- Show bundler output by default for eas update command. ([#1171](https://github.com/expo/eas-cli/pull/1171) by [@kgc00](https://github.com/kgc00))
- Allow users to skip metadata validation. ([#1175](https://github.com/expo/eas-cli/pull/1175) by [@byCedric](https://github.com/byCedric))
- Add experimental `--resource-class` flag for Build commands. ([#1138](https://github.com/expo/eas-cli/pull/1138) by [@christopherwalter](https://github.com/christopherwalter))
- Truncate long messages for `eas update` command, rather than failing. ([#1178](https://github.com/expo/eas-cli/pull/1178) by [@kgc00](https://github.com/kgc00))
- Add review details to metadata store configuration. ([#1184](https://github.com/expo/eas-cli/pull/1184) by [@byCedric](https://github.com/byCedric))
- Override applicationId via env. ([#1203](https://github.com/expo/eas-cli/pull/1203) by [@wkozyra95](https://github.com/wkozyra95))
### 🐛 Bug fixes
- Fix submission archive URLs. ([#1179](https://github.com/expo/eas-cli/pull/1179) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores
- Update validation rules for metadata info locales. ([#1174](https://github.com/expo/eas-cli/pull/1174) by [@byCedric](https://github.com/byCedric))
- Improve store configuration defaults and schema documentation. ([#1183](https://github.com/expo/eas-cli/pull/1183) by [@byCedric](https://github.com/byCedric))
- Improve store config categories configuration. ([#1187](https://github.com/expo/eas-cli/pull/1187) by [@byCedric](https://github.com/byCedric))
- Remove deprecated idfa and price tier from store config. ([#1193](https://github.com/expo/eas-cli/pull/1193) by [@byCedric](https://github.com/byCedric))
## [0.54.1](https://github.com/expo/eas-cli/releases/tag/v0.54.1) - 2022-06-15
### 🎉 New features
- Add App Store Connect link after `eas metadata:push`. ([#1168](https://github.com/expo/eas-cli/pull/1168) by [@byCedric](https://github.com/byCedric))
### 🐛 Bug fixes
- Surface ASC errors in metadata for normal API rejections. ([#1167](https://github.com/expo/eas-cli/pull/1167) by [@byCedric](https://github.com/byCedric))
- Add metadata schema to bundled eas-cli package. ([#1166](https://github.com/expo/eas-cli/pull/1166) by [@byCedric](https://github.com/byCedric))
## [0.54.0](https://github.com/expo/eas-cli/releases/tag/v0.54.0) - 2022-06-15
### 🛠 Breaking changes
- Remove fallback for legacy format in `eas.json`. ([#1158](https://github.com/expo/eas-cli/pull/1158), [#1163](https://github.com/expo/eas-cli/pull/1163) by [@wkozyra95](https://github.com/wkozyra95))
### 🎉 New features
- `eas update` now provides more information about the publish process including real-time feedback on asset uploads, update ids, and website links. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
- Added first beta of `eas metadata` to sync store information using store configuration files ([#1136](https://github.com/expo/eas-cli/pull/1136) by [@bycedric](https://github.com/bycedric))
### 🐛 Bug fixes
- Improved support for working on branches with many updates. ([#1144](https://github.com/expo/eas-cli/pull/1144), [#1148](https://github.com/expo/eas-cli/pull/1148) by [@kgc00](https://github.com/kgc00))
- `eas update` project links no longer contain a `)` on unsupported terminals. ([#1152](https://github.com/expo/eas-cli/pull/1152) by [@kgc00](https://github.com/kgc00))
### 🧹 Chores
- Update `eas-build-job`. ([#1162](https://github.com/expo/eas-cli/pull/1162) by [@wkozyra95](https://github.com/wkozyra95))
## [0.53.1](https://github.com/expo/eas-cli/releases/tag/v0.53.1) - 2022-06-07
### 🐛 Bug fixes
- No longer timeout on publishing updates for branches with many updates. ([#1119](https://github.com/expo/eas-cli/pull/1119) by [@kgc00](https://github.com/kgc00))
### 🧹 Chores
- Display project archive docs before compressing/uploading to EAS Build. ([#1127](https://github.com/expo/eas-cli/pull/1127) by [@dsokal](https://github.com/dsokal))
- Add new common error code for submits. ([#1129](https://github.com/expo/eas-cli/pull/1129) by [@dsokal](https://github.com/dsokal))
## [0.53.0](https://github.com/expo/eas-cli/releases/tag/v0.53.0) - 2022-05-30
### 🛠 Breaking changes
- Drop support for Node < 14. ([#1098](https://github.com/expo/eas-cli/pull/1098) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes
- Match bundle identifier capabilities more accurately. ([#1112](https://github.com/expo/eas-cli/pull/1112) by [@EvanBacon](https://github.com/EvanBacon))
- Limit concurrent asset uploads. ([#1153](https://github.com/expo/eas-cli/pull/1153) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Update dependencies. ([#1095](https://github.com/expo/eas-cli/pull/1095), [#1115](https://github.com/expo/eas-cli/pull/1115) by [@dsokal](https://github.com/dsokal))
- Better spinner placement for multiple builds. ([#1105](https://github.com/expo/eas-cli/pull/1105) by [@dsokal](https://github.com/dsokal))
- Use getExpoConfig to access config. ([#1122](https://github.com/expo/eas-cli/pull/1122) by [@wkozyra95](https://github.com/wkozyra95))
## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26 ## [0.52.0](https://github.com/expo/eas-cli/releases/tag/v0.52.0) - 2022-04-26
### 🎉 New features ### 🎉 New features
@@ -823,7 +159,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores ### 🧹 Chores
- Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot), [@dsokal](https://github.com/dsokal)) - Upgrade `node-forge` to 1.0.0. ([#902](https://github.com/expo/eas-cli/pull/902) by [@dependabot](https://github.com/apps/dependabot) + [@dsokal](https://github.com/dsokal))
- Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal)) - Replace `@expo/plugin-autocomplete` with `@oclif/plugin-autocomplete`. Upgrade oclif deps. ([#903](https://github.com/expo/eas-cli/pull/903) by [@dsokal](https://github.com/dsokal))
## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11 ## [0.44.0](https://github.com/expo/eas-cli/releases/tag/v0.44.0) - 2022-01-11
@@ -875,7 +211,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates. ([#854](https://github.com/expo/eas-cli/pull/854) by [@jonsamp](https://github.com/jonsamp)) - Adds commands for EAS Update, which is now in preview for subscribers. EAS Update makes fixing small bugs and pushing quick fixes a snap in between app store submissions. It accomplishes this by allowing an end-user's app to swap out the non-native parts of their app (for example, JS, styling, and image changes) with a new update that contains bug fixes and other updates.
- Adds `eas update`, which can bundle and publish updates on a branch. - Adds `eas update`, which can bundle and publish updates on a branch.
- Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels. - Adds `eas branch`, which manages branches. Branches contain a list of updates and are linked to channels.
- Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds. - Adds `eas channel`, which manages channels. Channels are specified inside builds and are linked to branches, allowing us to link specific updates with specific builds.
@@ -921,7 +257,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes ### 🐛 Bug fixes
- Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95)) - Fix submit for multi-target projects where ascAppId is not specified. ([#809](https://github.com/expo/eas-cli/pull/809) by [@wkozyra95](https://github.com/wkozyra95))
- Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales), [@dsokal](https://github.com/dsokal)) - Fix building iOS projects with Apple Watch companion app. ([#812](https://github.com/expo/eas-cli/pull/812), [#817](https://github.com/expo/eas-cli/pull/817), [#821](https://github.com/expo/eas-cli/pull/821) by [@jkhales](https://github.com/jkhales) + [@dsokal](https://github.com/dsokal))
### 🧹 Chores ### 🧹 Chores
@@ -957,7 +293,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pull/775) by [@EvanBacon](https://github.com/EvanBacon)) - Improve unknown capability syncing error message. ([#775](https://github.com/expo/eas-cli/pulls/775) by [@EvanBacon](https://github.com/EvanBacon))
- Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal)) - Add submit webhooks. ([#777](https://github.com/expo/eas-cli/pull/777) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes ### 🐛 Bug fixes
@@ -1055,7 +391,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes ### 🛠 Breaking changes
- Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701), [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal)) - Use new build job format. ([#701](https://github.com/expo/eas-cli/pull/701) and [#711](https://github.com/expo/eas-cli/pull/711) by [@dsokal](https://github.com/dsokal))
- Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal)) - Don't print logs to `stderr`. ([#708](https://github.com/expo/eas-cli/pull/708) by [@dsokal](https://github.com/dsokal))
- Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95)) - Remove automatic migration for legacy `eas.json` format introduced in [v0.22.2](https://github.com/expo/eas-cli/releases/tag/v0.22.2). ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
- Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95)) - Implement no-commit build workflow. Add required `cli` field in root of `eas.json`. ([#695](https://github.com/expo/eas-cli/pull/695) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1087,7 +423,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677) by [@jonsamp](https://github.com/jonsamp)) - Make "production" the default profile for building and submitting. ([#677](https://github.com/expo/eas-cli/pull/677)) by [@jonsamp](https://github.com/jonsamp)
- Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI. - Previously, the default profile when running `eas build` or `eas submit` was "release". We're changing it to a more recognizable name that is consistent with our docs, which is now "production". You can always specify a profile manually with the `--profile` flag. For this major version, if you do not have a profile named "production", and still have a profile named "release", we will fall back to the "release" profile as the default, however you'll see a warning as we're going to remove that behavior in the next major release of EAS CLI.
- To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles: - To upgrade, update **eas.json** to have a "production" profile in both the `build` and `submit` objects. If you already have a project set up, this will replace the existing "release" profile. After the change, eas.json should have the following profiles:
```json ```json
@@ -1133,7 +469,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🛠 Breaking changes ### 🛠 Breaking changes
- Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95), [@dsokal](https://github.com/dsokal)) - Don't resolve the iOS builder image on the client side. EAS Build will use the appropriate iOS image for a given Expo SDK version unless the `image` is defined. This only applies to managed projects. ([#675](https://github.com/expo/eas-cli/pull/675) by [@wkozyra95](https://github.com/wkozyra95) + [@dsokal](https://github.com/dsokal))
### 🎉 New features ### 🎉 New features
@@ -1145,7 +481,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes ### 🐛 Bug fixes
- Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal)) - Fix `--json` flag when running EAS CLI on GitHub actions. ([#669](https://github.com/expo/eas-cli/pull/669) by [@dsokal](https://github.com/dsokal))
- Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project. ([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne)) - Fix `"ios: mods.ios.infoPlist: Failed to find Info.plist linked to Xcode project."` warning when running `eas build` in a managed project.([#670](https://github.com/expo/eas-cli/pull/670) by [@brentvatne](https://github.com/brentvatne))
- Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal)) - Fix building monorepo projects on Windows. ([#671](https://github.com/expo/eas-cli/pull/671) by [@dsokal](https://github.com/dsokal))
## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05 ## [0.30.0](https://github.com/expo/eas-cli/releases/tag/v0.30.0) - 2021-10-05
@@ -1279,7 +615,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes ### 🐛 Bug fixes
- Fix wrong warning for `metro.config.js` check on Windows. ([#588](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv)) - Fix wrong warning for `metro.config.js` check on Windows. ([#587](https://github.com/expo/eas-cli/pull/588) by [@louisgv](https://github.com/louisgv))
- Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95)) - Fix detecting `googleServicesFile` with `EAS_NO_VCS=1`. ([#583](https://github.com/expo/eas-cli/pull/583) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores ### 🧹 Chores
@@ -1359,7 +695,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Include the file extension for update's assets in the manifest fragment. ([#535](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales)) - Include the file extension for update's assets in the manifest fragment. ([#532](https://github.com/expo/eas-cli/pull/535) by [@jkhales](https://github.com/jkhales))
- Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal)) - Use env variables from `eas.json` when evaluating `app.config.js`. ([#532](https://github.com/expo/eas-cli/pull/532) by [@dsokal](https://github.com/dsokal))
### 🐛 Bug fixes ### 🐛 Bug fixes
@@ -1379,7 +715,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric)) - Add `secret:create --force` command to overwrite existing secrets. ([#513](https://github.com/expo/eas-cli/pull/513) by [@bycedric](https://github.com/bycedric))
- Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal)) - Fall back to APK when building for internal distribution. ([#527](https://github.com/expo/eas-cli/pull/527) by [@dsokal](https://github.com/dsokal))
- Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal)) - Add `iosEnterpriseProvisioning` to build metadata. ([#522](https://github.com/expo/eas-cli/pull/522) by [@dsokal](https://github.com/dsokal))
- Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/520) by [@barthap](https://github.com/barthap)) - Autodetect Google Services JSON key path in `eas submit -p android`. ([#520](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
### 🐛 Bug fixes ### 🐛 Bug fixes
@@ -1393,7 +729,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales)) - Add `project:*` commands. ([#500](https://github.com/expo/eas-cli/pull/500) by [@jkhales](https://github.com/jkhales))
- Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon)) - Added support for iOS 15 capabilities: Communication Notifications, Time Sensitive Notifications, Group Activities, and Family Controls. ([#499](https://github.com/expo/eas-cli/pull/499) by [@EvanBacon](https://github.com/EvanBacon))
- Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504), [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal)) - Show more build metadata in `build:view` and `build:list`. ([#504](https://github.com/expo/eas-cli/pull/504) and [#508](https://github.com/expo/eas-cli/pull/508) by [@dsokal](https://github.com/dsokal))
- Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal)) - Add runtime version to build metadata. ([#493](https://github.com/expo/eas-cli/pull/493) by [@dsokal](https://github.com/dsokal))
## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09 ## [0.20.0](https://github.com/expo/eas-cli/releases/tag/v0.20.0) - 2021-07-09
@@ -1473,15 +809,15 @@ This is the log of notable changes to EAS CLI and related packages.
- New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj)) - New version of Android credentials manager. ([#460](https://github.com/expo/eas-cli/pull/460) by [@quinlanj](https://github.com/quinlanj))
- Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson)) - Add an internal distribution with dev client to `build:configure` defaults. ([#465](https://github.com/expo/eas-cli/pull/465) by [@fson](https://github.com/fson))
- Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales)) - Add `updates.channel` to build metadata. ([#461](https://github.com/expo/eas-cli/pull/461) by [@jkhales](https://github.com/jkhales))
- iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469), [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj)) - iOS push key setup and management now available in `eas-cli credentials`. ([#469](https://github.com/expo/eas-cli/pull/469) [#470](https://github.com/expo/eas-cli/pull/470) by [@quinlanj](https://github.com/quinlanj))
- Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal)) - Support new build status: `new`. ([#475](https://github.com/expo/eas-cli/pull/475) by [@dsokal](https://github.com/dsokal))
### 🧹 Chores ### 🧹 Chores
- Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne)) - Deprecate `--skip-credentials-check` flag because it doesn't do anything and is no longer needed. ([#442](https://github.com/expo/eas-cli/pull/442) by [@brentvatne](https://github.com/brentvatne))
- Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439), [#440](https://github.com/expo/eas-cli/pull/440), [#438](https://github.com/expo/eas-cli/pull/438), [#443](https://github.com/expo/eas-cli/pull/443), [#447](https://github.com/expo/eas-cli/pull/447), [#451](https://github.com/expo/eas-cli/pull/451), [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj)) - Move android credentials code to new Graphql API. ([#439](https://github.com/expo/eas-cli/pull/439) [#440](https://github.com/expo/eas-cli/pull/440) [#438](https://github.com/expo/eas-cli/pull/438) [#443](https://github.com/expo/eas-cli/pull/443) [#447](https://github.com/expo/eas-cli/pull/447) [#451](https://github.com/expo/eas-cli/pull/451) [#455](https://github.com/expo/eas-cli/pull/455) by [@quinlanj](https://github.com/quinlanj))
- Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj)) - Prepare Graphql infra to support iOS push keys. ([#456](https://github.com/expo/eas-cli/pull/456) by [@quinlanj](https://github.com/quinlanj))
- Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448), [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj)) - Improve credentials DX ([#448](https://github.com/expo/eas-cli/pull/448) [#449](https://github.com/expo/eas-cli/pull/449) by [@quinlanj](https://github.com/quinlanj))
- Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson)) - Add analytics on dev client builds. ([#454](https://github.com/expo/eas-cli/pull/454) by [@fson](https://github.com/fson))
- Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95)) - Support non-git projects. ([#462](https://github.com/expo/eas-cli/pull/462) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1495,7 +831,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores ### 🧹 Chores
- Android credentials setup now on Graphql API. ([#427](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj)) - Android credentials setup now on Graphql API. ([#434](https://github.com/expo/eas-cli/pull/427) by [@quinlanj](https://github.com/quinlanj))
## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26 ## [0.16.0](https://github.com/expo/eas-cli/releases/tag/v0.16.0) - 2021-05-26
@@ -1527,7 +863,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon)) - Auto sync associated domains capability before building. ([#384](https://github.com/expo/eas-cli/pull/384) by [@EvanBacon](https://github.com/EvanBacon))
- Add eas init command. ([#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales)) - Add eas init command. [#402](https://github.com/expo/eas-cli/pull/402) by [@jkhales](https://github.com/jkhales))
- Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile)) - Allow for arbitrary string values in `schemeBuildConfiguration`. ([#394](https://github.com/expo/eas-cli/pull/394) by [@randomhajile](https://github.com/randomhajile))
- Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile)) - Allow for installing custom `expo-cli` version on EAS Build. ([#409](https://github.com/expo/eas-cli/pull/409) by [@randomhajile](https://github.com/randomhajile))
- Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95)) - Support PKCS kesytores for Android. ([#398](https://github.com/expo/eas-cli/pull/398) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1538,7 +874,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales)) - Fix failure when publishing without the platform flag. ([#415](https://github.com/expo/eas-cli/pull/415) by [@jkhales](https://github.com/jkhales))
- Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal)) - Pin versions in package.json. ([#399](https://github.com/expo/eas-cli/pull/399) by [@dsokal](https://github.com/dsokal))
- Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399). - Revert [0ac2f](https://github.com/expo/eas-cli/commit/0ac2fb77a118df609381c4e350aa68609340c3cd) as the root cause of the issue has been fixed in [#399](https://github.com/expo/eas-cli/pull/399).
- Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne)) - Include development-client in valid buildType for internal distribution. ([#410](https://github.com/expo/eas-cli/pull/410) by [@brentvatne](https://github.com/brentvatne))
## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11 ## [0.14.1](https://github.com/expo/eas-cli/releases/tag/v0.14.1) - 2021-05-11
@@ -1565,7 +901,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj)) - Cleanup unused code. ([#364](https://github.com/expo/eas-cli/pull/364), [#365](https://github.com/expo/eas-cli/pull/365) by [@quinlanj](https://github.com/quinlanj))
- Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj)) - Redesign UX in beta credentials manager. ([#360](https://github.com/expo/eas-cli/pull/360) by [@quinlanj](https://github.com/quinlanj))
- Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [#357](https://github.com/expo/eas-cli/pull/357), [#361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj)) - Port more options to the beta credentials manager. ([#352](https://github.com/expo/eas-cli/pull/352), [357](https://github.com/expo/eas-cli/pull/357), [361](https://github.com/expo/eas-cli/pull/361) by [@quinlanj](https://github.com/quinlanj))
- Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj)) - Add getBuildProfileNamesAsync helper to EasJsonReader. ([#351](https://github.com/expo/eas-cli/pull/351) by [@quinlanj](https://github.com/quinlanj))
- Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95)) - Increase build timeout to 1 hour. ([#370](https://github.com/expo/eas-cli/pull/370) by [@wkozyra95](https://github.com/wkozyra95))
- Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95)) - Remove check for pending builds. ([#373](https://github.com/expo/eas-cli/pull/373) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1626,7 +962,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336) by [@wschurman](https://github.com/wschurman)) - Use special Expo SDK runtime version for managed projects ([#336](https://github.com/expo/eas-cli/pull/336)) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes ### 🐛 Bug fixes
@@ -1651,7 +987,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features ### 🎉 New features
- Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales)) - Retry graphQL call on network or transient server errors. ([#320](https://github.com/expo/eas-cli/pull/320) by [@jkhales](https://github.com/jkhales))
- Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/311) by [@barthap](https://github.com/barthap)) - Display more friendly error messages when `eas submit` fails. ([#311](https://github.com/expo/eas-cli/pull/297) by [@barthap](https://github.com/barthap))
- Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal)) - Add support for managing webhooks (new commands: `webhook:create`, `webhook:view`, `webhook:list`, `webhook:update`, and `webhook:delete`). ([#314](https://github.com/expo/eas-cli/pull/314) by [@dsokal](https://github.com/dsokal))
- Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95)) - Support for local builds [experimental]. ([#305](https://github.com/expo/eas-cli/pull/305) by [@wkozyra95](https://github.com/wkozyra95))
@@ -1735,7 +1071,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes ### 🐛 Bug fixes
- Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95)) - Make sure all files are committed before build. ([#251](https://github.com/expo/eas-cli/pull/251) by [@wkozyra95](https://github.com/wkozyra95))
- Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257) by [@wkozyra95](https://github.com/wkozyra95)) - Fix `eas submit` support for tar.gz files. ([#257](https://github.com/expo/eas-cli/pull/257)) by [@wkozyra95](https://github.com/wkozyra95))
- Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95)) - Show untracked files when checking `git status`. ([#259](https://github.com/expo/eas-cli/pull/259) by [@wkozyra95](https://github.com/wkozyra95))
### 🧹 Chores ### 🧹 Chores
@@ -1762,7 +1098,7 @@ This is the log of notable changes to EAS CLI and related packages.
### 🐛 Bug fixes ### 🐛 Bug fixes
- Fix `"buildType" is not allowed` error. ([595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95)) - Fix `"buildType" is not allowed` error. ([#595bf](https://github.com/expo/eas-cli/commit/595bfecf1cbff0f76e7fd2049fe16f6f38bbe150) by [@wkozyra95](https://github.com/wkozyra95))
## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16 ## [0.4.0](https://github.com/expo/eas-cli/releases/tag/v0.4.0) - 2021-02-16
+2 -11
View File
@@ -1,16 +1,7 @@
* @dsokal @wkozyra95
packages/eas-cli/src/build @dsokal @wkozyra95 packages/eas-cli/src/build @dsokal @wkozyra95
packages/eas-cli/src/commands/build @dsokal @wkozyra95 packages/eas-cli/src/commands/build @dsokal @wkozyra95
packages/eas-cli/src/submit @dsokal packages/eas-cli/src/submit @barthap @dsokal
packages/eas-cli/src/commands/submit.ts @dsokal packages/eas-cli/src/commands/submit.ts @barthap @dsokal
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @dsokal @wkozyra95 packages/eas-json @dsokal @wkozyra95
+3 -47
View File
@@ -1,48 +1,4 @@
# 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. 1. Run `yarn release` in the repository root folder.
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.
> 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).
-18
View File
@@ -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`.
+130
View File
@@ -0,0 +1,130 @@
#!/bin/bash
# This script downloads the latest release version of EAS CLI from GitHub and
# installs it in /usr/local/bin
{
# All code is inside a block to ensure the script executes only when
# downloaded completely.
set -euo pipefail
prefix="${EAS_PREFIX:-/usr/local}"
temp_dir="$(mktemp -d)"
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]
then
# Use colors when stdout is a terminal and NO_COLOR isn't set.
text_bold=$'\033[1m'
text_normal=$'\033[22m'
text_red=$'\033[31m'
text_default_color=$'\033[39m'
else
# Disable colors
text_bold=""
text_normal=""
text_red=""
text_default_color=""
fi
abort() {
echo -e "$text_bold$text_red$1$text_normal$text_default_color"
exit 1
}
install() {
needs_sudo=""
# Make sure path exists and can be written to by the current user.
if ! mkdir -p "$prefix/lib" || [[ ! -w "$prefix/lib" ]]
then
needs_sudo=true
fi
if ! mkdir -p "$prefix/bin" || [[ ! -w "$prefix/bin" ]]
then
needs_sudo=true
fi
if ! rm -rf "$prefix/lib/eas"
then
needs_sudo=true
fi
# Run commands with sudo if necessary.
if [ -n "$needs_sudo" ]
then
if [ "$EUID" -ne 0 ]
then
echo "Installing requires superuser access."
echo "The sudo command will prompt for your password."
fi
sudo rm -rf "$prefix/lib/eas"
sudo mv "$temp_dir/eas" "$prefix/lib/eas"
sudo ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
else
mv "$temp_dir/eas" "$prefix/lib/eas"
ln -fs "$prefix/lib/eas/bin/eas" "$prefix/bin/eas"
fi
}
if [[ ! ":$PATH:" == *":$prefix/bin:"* ]]; then
abort "Your path is missing $prefix/bin, you need to add this to \$PATH use this installer."
fi
hardware="$(uname -m)"
case "$hardware" in
x86_64)
arch="x64"
;;
arm*)
arch="arm"
;;
*)
abort "Unsupported CPU architecture $hardware."
;;
esac
case "$(uname -s)" in
Darwin*)
platform="darwin"
if [[ "$arch" != "x64" ]]; then
abort "Unsupported CPU architecture $hardware on macOS."
fi
;;
Linux*)
platform="linux"
;;
*)
abort "This installer is supported only on macOS, Linux, and Windows Subsystem for Linux."
;;
esac
url=$(hub release -L 1 | xargs hub release show --show-downloads | grep "${platform}-${arch}.tar.gz")
echo "Installing EAS CLI from $url"
mkdir -p "$temp_dir"
cd "$temp_dir"
echo
echo "Downloading..."
if [ "$(command -v curl)" ]
then
curl --location --progress-bar "$url" | tar xz
else
wget -O- "$url" | tar xz
fi
# Move the installation over and link the executable (may require sudo)
install
# Test the bundled node binary and if it doesn't work remove it (fall back to
# node installed in path).
"$prefix/lib/eas/bin/node" -v > /dev/null || {
rm "$prefix/lib/eas/bin/node"
}
# Test the CLI
executable=$(command -v eas) || abort "Could not run eas after installation."
echo
echo -e "EAS CLI is installed to ${executable}"
eas --version
echo
echo -e "Type ${text_bold}eas --help${text_normal} to get started."
}
+1 -2
View File
@@ -1,6 +1,5 @@
{ {
"npmClient": "yarn", "npmClient": "yarn",
"useWorkspaces": true, "useWorkspaces": true,
"version": "3.9.3", "version": "0.52.0"
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
} }
+16 -18
View File
@@ -1,5 +1,4 @@
{ {
"name": "eas-cli-root",
"private": true, "private": true,
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
@@ -9,32 +8,31 @@
"build:tarballs:linux": "yarn build-for-size-check", "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-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck", "typecheck": "lerna run typecheck",
"start": "lerna run watch --stream --parallel", "start": "lerna run watch --parallel",
"watch": "yarn start", "watch": "yarn start",
"eas": "packages/eas-cli/bin/run", "eas": "packages/eas-cli/bin/run",
"lint": "eslint .", "lint": "eslint .",
"lint-changelog": "./scripts/bin/run lint-changelog", "release": "./scripts/bin/run update-local-plugin && lerna version --exact",
"release": "lerna run rebuild && ./scripts/bin/run release",
"test": "jest", "test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage" "clean": "lerna run clean && rm -rf node_modules coverage"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "29.2.5", "@types/jest": "27.4.1",
"@types/node": "18.11.18", "@types/node": "16.7.8",
"eslint": "8.31.0", "eslint": "8.12.0",
"eslint-config-universe": "11.1.1", "eslint-config-universe": "10.0.0",
"eslint-plugin-async-protect": "3.0.0", "eslint-plugin-async-protect": "1.1.0",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"jest": "29.3.1", "jest": "27.5.1",
"jest-watch-typeahead": "2.2.1", "jest-watch-typeahead": "1.0.0",
"lerna": "6.4.1", "lerna": "4.0.0",
"oclif": "3.4.3", "oclif": "2.6.3",
"prettier": "2.8.2", "prettier": "2.6.1",
"ts-jest": "29.0.3", "ts-jest": "27.1.4",
"typescript": "4.9.4" "typescript": "4.6.3"
}, },
"volta": { "volta": {
"node": "18.6.0", "node": "16.10.0",
"yarn": "1.22.19" "yarn": "1.22.15"
} }
} }
-18
View File
@@ -2,24 +2,6 @@ const rootEslintrc = require('../../.eslintrc.js');
module.exports = { module.exports = {
rules: { 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': [ 'no-restricted-imports': [
'error', 'error',
{ {
+128 -447
View File
File diff suppressed because it is too large. Load diff
@@ -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 () => { content="/**
const generatedCode = await fs.readFile(path, 'utf8');
const comment = `/**
* This file was generated using GraphQL Codegen * This file was generated using GraphQL Codegen
* Command: yarn generate-graphql-code * Command: yarn generate-graphql-code
* Run this during development for automatic type generation when editing GraphQL documents * Run this during development for automatic type generation when editing GraphQL documents
* For more info and docs, visit https://graphql-code-generator.com/ * 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
})();
@@ -0,0 +1,7 @@
{
"extends": "@tsconfig/node16/tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"strict": true
}
}
+2 -4
View File
@@ -1,12 +1,10 @@
overwrite: true overwrite: true
schema: 'https://staging-api.expo.dev/graphql' schema: 'http://localhost:3000/graphql'
documents: documents:
- 'src/graphql/**/!(*.d).{ts,tsx}' - 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}' - 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}' - 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts' - 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
generates: generates:
src/graphql/generated.ts: src/graphql/generated.ts:
plugins: plugins:
@@ -16,7 +14,7 @@ generates:
dedupeOperationSuffix: true dedupeOperationSuffix: true
hooks: hooks:
afterOneFileWrite: afterOneFileWrite:
- 'node ./scripts/annotate-graphql-codegen.js' - ./annotate-graphql-codegen.sh
./graphql.schema.json: ./graphql.schema.json:
plugins: plugins:
- 'introspection' - 'introspection'
+798 -11802
View File
File diff suppressed because it is too large. Load diff
+75 -87
View File
@@ -1,130 +1,126 @@
{ {
"name": "eas-cli", "name": "eas-cli",
"description": "EAS command line tool", "description": "EAS command line tool",
"version": "3.9.3", "version": "0.52.0",
"author": "Expo <[email protected]>", "author": "Expo <[email protected]>",
"bin": { "bin": {
"eas": "./bin/run" "eas": "./bin/run"
}, },
"bugs": "https://github.com/expo/eas-cli/issues", "bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": { "dependencies": {
"@expo/apple-utils": "1.2.0", "@expo/apple-utils": "0.0.0-alpha.31",
"@expo/code-signing-certificates": "0.0.5", "@expo/code-signing-certificates": "0.0.2",
"@expo/config": "7.0.3", "@expo/config": "6.0.21",
"@expo/config-plugins": "5.0.4", "@expo/config-plugins": "4.1.2",
"@expo/config-types": "47.0.0", "@expo/config-types": "44.0.0",
"@expo/eas-build-job": "1.0.7", "@expo/eas-build-job": "0.2.71",
"@expo/eas-json": "3.9.2", "@expo/eas-json": "0.52.0",
"@expo/json-file": "8.2.37", "@expo/json-file": "8.2.34",
"@expo/multipart-body-parser": "1.1.0", "@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33", "@expo/package-manager": "0.0.50",
"@expo/package-manager": "0.0.57", "@expo/pkcs12": "0.0.7",
"@expo/pkcs12": "0.0.8", "@expo/plist": "0.0.17",
"@expo/plist": "0.0.20", "@expo/plugin-warn-if-update-available": "2.1.0",
"@expo/plugin-help": "5.1.22", "@expo/prebuild-config": "3.1.2",
"@expo/plugin-warn-if-update-available": "2.5.0",
"@expo/prebuild-config": "5.0.7",
"@expo/results": "1.0.0", "@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1", "@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0", "@expo/spawn-async": "1.6.0",
"@expo/steps": "1.0.8",
"@expo/timeago.js": "1.0.0", "@expo/timeago.js": "1.0.0",
"@oclif/core": "1.23.2", "@oclif/core": "1.6.4",
"@oclif/plugin-autocomplete": "1.3.10", "@oclif/plugin-autocomplete": "1.2.0",
"@segment/ajv-human-errors": "^2.1.2", "@oclif/plugin-help": "5.1.12",
"@urql/core": "3.1.1", "@urql/core": "2.4.3",
"@urql/exchange-retry": "1.0.0", "@urql/exchange-retry": "0.3.2",
"ajv": "8.11.0", "adm-zip": "0.5.9",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"chalk": "4.1.2", "chalk": "4.1.2",
"cli-progress": "3.11.2", "cli-progress": "3.10.0",
"cli-table3": "0.6.1",
"dateformat": "4.6.3", "dateformat": "4.6.3",
"dotenv": "16.0.3",
"env-paths": "2.2.0", "env-paths": "2.2.0",
"envinfo": "7.8.1", "envinfo": "7.8.1",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"fast-glob": "3.2.12", "fast-glob": "3.2.11",
"figures": "3.2.0", "figures": "3.2.0",
"form-data": "4.0.0", "form-data": "4.0.0",
"fs-extra": "10.1.0", "fs-extra": "10.0.1",
"getenv": "1.0.0", "getenv": "1.0.0",
"gradle-to-js": "2.0.1", "got": "^11.1.4",
"graphql": "16.6.0", "gradle-to-js": "2.0.0",
"graphql": "16.3.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1", "https-proxy-agent": "5.0.0",
"ignore": "5.2.4", "ignore": "5.2.0",
"indent-string": "4.0.0", "indent-string": "4.0.0",
"jks-js": "1.1.0", "jks-js": "1.0.2",
"joi": "17.7.0", "js-yaml": "^3.13.1",
"joi": "17.6.0",
"keychain": "1.3.0", "keychain": "1.3.0",
"log-symbols": "4.1.0", "log-symbols": "4.1.0",
"mime": "3.0.0", "mime": "3.0.0",
"minimatch": "5.1.2", "minimatch": "3.0.5",
"nanoid": "3.3.4", "nanoid": "3.3.2",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"nullthrows": "1.1.1", "nullthrows": "1.1.1",
"ora": "5.1.0", "ora": "5.1.0",
"pkg-dir": "4.2.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.4.2",
"qrcode-terminal": "0.12.0", "qrcode-terminal": "0.12.0",
"pnp-webpack-plugin": "1.7.0",
"react-dev-utils": "12.0.0",
"resolve-from": "5.0.0", "resolve-from": "5.0.0",
"semver": "7.3.8", "semver": "7.3.5",
"slash": "3.0.0", "slash": "3.0.0",
"tar": "6.1.13", "strip-ansi": "6.0.0",
"tar": "6.1.11",
"terminal-link": "2.1.1", "terminal-link": "2.1.1",
"tslib": "2.4.1", "ts-loader": "^9.2.8",
"tslib": "2.3.1",
"turndown": "7.1.1", "turndown": "7.1.1",
"untildify": "4.0.0", "untildify": "4.0.0",
"uuid": "9.0.0", "uuid": "8.3.2",
"webpack": "5.72.0",
"wrap-ansi": "7.0.0" "wrap-ansi": "7.0.0"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "2.16.3", "@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/introspection": "2.2.3", "@graphql-codegen/introspection": "2.1.1",
"@graphql-codegen/typescript": "2.8.7", "@graphql-codegen/typescript": "2.4.8",
"@graphql-codegen/typescript-operations": "2.5.12", "@graphql-codegen/typescript-operations": "2.3.5",
"@types/cli-progress": "3.11.0", "@types/cli-progress": "3.9.2",
"@types/adm-zip": "0.4.34",
"@types/dateformat": "3.0.1", "@types/dateformat": "3.0.1",
"@types/envinfo": "7.8.1", "@types/envinfo": "7.8.1",
"@types/express": "4.17.15",
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0", "@types/getenv": "^1.0.0",
"@types/mime": "3.0.1", "@types/js-yaml": "^3.12.2",
"@types/node-fetch": "2.6.2", "@types/mime": "2.0.3",
"@types/node-forge": "1.3.1", "@types/node-fetch": "^2.5.7",
"@types/pngjs": "6.0.1", "@types/node-forge": "1.0.1",
"@types/promise-retry": "1.1.3", "@types/prompts": "2.0.14",
"@types/prompts": "2.4.2", "@types/semver": "7.3.9",
"@types/semver": "7.3.13", "@types/tar": "6.1.1",
"@types/tar": "6.1.3", "@types/tough-cookie": "4.0.1",
"@types/tough-cookie": "4.0.2", "@types/uuid": "8.3.4",
"@types/uuid": "9.0.0", "@types/webpack": "5.28.0",
"@types/wrap-ansi": "3.0.0", "@types/wrap-ansi": "3.0.0",
"axios": "0.27.2", "axios": "0.26.1",
"eslint-plugin-graphql": "4.0.0", "eslint-plugin-graphql": "4.0.0",
"express": "4.18.2", "form-data": "4.0.0",
"memfs": "3.4.13", "memfs": "3.4.1",
"mockdate": "3.0.5", "mockdate": "3.0.5",
"nock": "13.2.9", "nock": "13.2.4",
"rimraf": "3.0.2", "tempy": "0.7.0",
"ts-deepmerge": "4.0.0", "ts-deepmerge": "2.0.1",
"ts-mockito": "2.6.1", "typescript": "4.6.3"
"ts-node": "10.9.1",
"typescript": "4.9.4"
}, },
"engines": { "engines": {
"node": ">=14.0.0" "node": ">=12.0.0"
}, },
"files": [ "files": [
"/bin", "/bin",
"/build", "/build",
"/oclif.manifest.json", "/oclif.manifest.json"
"/schema"
], ],
"homepage": "https://github.com/expo/eas-cli", "homepage": "https://github.com/expo/eas-cli",
"keywords": [ "keywords": [
@@ -138,7 +134,7 @@
"commands": "./build/commands", "commands": "./build/commands",
"plugins": [ "plugins": [
"@oclif/plugin-autocomplete", "@oclif/plugin-autocomplete",
"@expo/plugin-help", "@oclif/plugin-help",
"@expo/plugin-warn-if-update-available" "@expo/plugin-warn-if-update-available"
], ],
"topics": { "topics": {
@@ -157,9 +153,6 @@
"device": { "device": {
"description": "manage Apple devices for Internal Distribution" "description": "manage Apple devices for Internal Distribution"
}, },
"metadata": {
"description": "manage store configuration"
},
"project": { "project": {
"description": "manage project" "description": "manage project"
}, },
@@ -178,11 +171,11 @@
}, },
"warn-if-update-available": { "warn-if-update-available": {
"timeoutInDays": 0.5, "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": { "update": {
"node": { "node": {
"version": "18.6.0" "version": "12.13.0"
}, },
"s3": { "s3": {
"templates": { "templates": {
@@ -202,9 +195,8 @@
}, },
"repository": "expo/eas-cli", "repository": "expo/eas-cli",
"scripts": { "scripts": {
"postpack": "rimraf oclif.manifest.json", "postpack": "rm -f oclif.manifest.json",
"prepack": "yarn rebuild && node ./scripts/prepack.js", "prepack": "./scripts/prepack.sh",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh", "pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json", "build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput", "watch": "yarn build --watch --preserveWatchOutput",
@@ -212,10 +204,6 @@
"test": "jest", "test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md", "version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml", "generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"clean": "rimraf dist build tmp node_modules yarn-error.log" "clean": "rm -rf dist build tmp node_modules yarn-error.log"
},
"volta": {
"node": "18.6.0",
"yarn": "1.22.19"
} }
} }
-793
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
const fs = require('fs/promises'); const { promises: fs } = require('fs');
(async () => { (async () => {
// Patch `oclif readme` path and link generation // Patch `oclif readme` path and link generation
-7
View File
@@ -1,7 +0,0 @@
const spawn = require('@expo/spawn-async');
(async () => {
if (!process.env.CLI_SIZE_CHECK) {
await spawn('yarn', ['oclif', 'manifest'], { stdio: 'inherit' });
}
})();
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -eo pipefail
rm -rf build
yarn build
if [ -z "$CLI_SIZE_CHECK" ]; then
yarn oclif manifest
fi
-23
View File
@@ -1,23 +0,0 @@
import { JSONValue } from '@expo/json-file';
export class ApiV2Error extends Error {
override readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
+4 -9
View File
@@ -1,10 +1,9 @@
import assert from 'assert'; import assert from 'assert';
import nock from 'nock'; import nock from 'nock';
import { ApiV2Error } from '../ApiV2Error'; import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
describe(ApiV2Client, () => { describe('apiClient', () => {
it('converts Expo APIv2 error to ApiV2Error', async () => { it('converts Expo APIv2 error to ApiV2Error', async () => {
nock(getExpoApiBaseUrl()) nock(getExpoApiBaseUrl())
.post('/v2/test') .post('/v2/test')
@@ -22,9 +21,7 @@ describe(ApiV2Client, () => {
let error: Error | null = null; let error: Error | null = null;
try { try {
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', { await api.postAsync('test', { body: {} });
body: {},
});
} catch (e: any) { } catch (e: any) {
error = e; error = e;
} }
@@ -44,9 +41,7 @@ describe(ApiV2Client, () => {
let error: Error | null = null; let error: Error | null = null;
try { try {
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', { await api.postAsync('test', { body: {} });
body: {},
});
} catch (e: any) { } catch (e: any) {
error = e; error = e;
} }
@@ -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),
},
};
}
@@ -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 },
};
}
}
+11 -16
View File
@@ -1,28 +1,23 @@
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager'; import { Analytics, Event } from './events';
export type TrackingContext = Record<string, string | number | boolean>;
export async function withAnalyticsAsync<Result>( export async function withAnalyticsAsync<Result>(
analytics: Analytics,
fn: () => Promise<Result>, fn: () => Promise<Result>,
{ analytics: {
attemptEvent, attemptEvent: Event;
successEvent, successEvent: Event;
failureEvent, failureEvent: Event;
properties, trackingCtx: TrackingContext;
}: {
attemptEvent: AnalyticsEvent;
successEvent: AnalyticsEvent;
failureEvent: AnalyticsEvent;
properties: AnalyticsEventProperties;
} }
): Promise<Result> { ): Promise<Result> {
try { try {
analytics.logEvent(attemptEvent, properties);
const result = await fn(); const result = await fn();
analytics.logEvent(successEvent, properties); Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
return result; return result;
} catch (error: any) { } catch (error: any) {
analytics.logEvent(failureEvent, { Analytics.logEvent(analytics.failureEvent, {
...properties, ...analytics.trackingCtx,
reason: error.message, reason: error.message,
}); });
throw error; throw error;
+52
View File
@@ -0,0 +1,52 @@
import { logEvent } from './rudderstackClient';
export type Event = BuildEvent | SubmissionEvent;
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',
}
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 class Analytics {
static logEvent(name: Event, properties: Record<string, any>): void {
logEvent(name, properties);
}
}
@@ -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
}
+74 -59
View File
@@ -1,78 +1,93 @@
import { JSONValue } from '@expo/json-file'; import { JSONValue } from '@expo/json-file';
import { ApiV2Error } from './ApiV2Error';
import fetch, { RequestError, RequestInit } from './fetch'; import fetch, { RequestError, RequestInit } from './fetch';
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
export class ApiV2Error extends Error {
readonly name = 'ApiV2Error';
readonly expoApiV2ErrorCode: string;
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
constructor(response: {
message: string;
code: string;
stack?: string;
details?: JSONValue;
metadata?: object;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
}
}
interface RequestOptions { interface RequestOptions {
body: JSONValue; body: JSONValue;
} }
export class ApiV2Client { export const api = {
constructor( async putAsync(path: string, options: RequestOptions): Promise<any> {
private readonly authInfo: { return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
accessToken: string | null; },
sessionSecret: string | null;
}
) {}
public async putAsync(path: string, options: RequestOptions): Promise<any> { async postAsync(path: string, options: RequestOptions): Promise<any> {
return await this.requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) }); return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
},
async deleteAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'DELETE' });
},
async getAsync(path: string): Promise<any> {
return await requestAsync(path, { method: 'GET' });
},
};
async function requestAsync(path: string, options: RequestInit): Promise<any> {
try {
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
...getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await handleApiErrorAsync(err);
} }
}
public async postAsync(path: string, options: RequestOptions): Promise<any> { async function handleApiErrorAsync(err: any): Promise<void> {
return await this.requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) }); if (err instanceof RequestError) {
} let result: { [key: string]: any };
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 { try {
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, { result = (await err.response.json()) as { [key: string]: any };
...options, } catch {
headers: { throw new Error(`Malformed api response: ${await err.response.text()}`);
'Content-Type': 'application/json',
...this.getAuthHeaders(),
},
});
return await response.json();
} catch (err) {
await ApiV2Client.handleApiErrorAsync(err);
} }
if (result.errors?.length) {
throw new ApiV2Error(result.errors[0]);
}
} else {
throw err;
} }
}
private static async handleApiErrorAsync(err: any): Promise<void> { function getAuthHeaders(): Record<string, string> {
if (err instanceof RequestError) { const token = getAccessToken();
let result: { [key: string]: any }; if (token) {
try { return { authorization: `Bearer ${token}` };
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;
}
} }
const sessionSecret = getSessionSecret();
if (sessionSecret) {
return { 'expo-session': sessionSecret };
}
return {};
} }
export function getExpoApiBaseUrl(): string { export function getExpoApiBaseUrl(): string {
-184
View File
@@ -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;
}
}
}
-14
View File
@@ -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,362 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { CombinedError } from '@urql/core';
import { GraphQLError } from 'graphql/error';
import { EasCommandError } from '../../commandUtils/errors';
import Build from '../../commands/build';
import Log, { link } from '../../log';
import { handleBuildRequestError } from '../build';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from '../errors';
beforeEach(() => {
jest.resetAllMocks();
});
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
{
"message": "Error 1"
},
{
"message": "Error 2"
},
{
"message": "Error 3"
}
]`;
const EXPECTED_EAS_BUILD_DOWN_MESSAGE = `EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID = `You have already reached the maximum number of pending Android builds for your account. Try again later.`;
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS = `You have already reached the maximum number of pending iOS builds for your account. Try again later.`;
const EXPECTED_GENERIC_MESSAGE =
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.';
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
return new GraphQLError(message, null, null, null, null, null, {
errorCode,
});
};
const assertReThrownError = (
caughtError: CombinedError | Error,
expectedType: typeof EasCommandError,
expectedMessage: string = 'Error 1'
): void => {
expect(caughtError).toBeInstanceOf(expectedType);
expect((caughtError as Error).message).toEqual(expectedMessage);
};
describe(Build.name, () => {
describe('handle build request errors', () => {
describe('logs graphQL errors to debug if present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
});
});
describe('logs nothing to debug if graphQLErrors not present', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const logDebugSpy = jest.spyOn(Log, 'debug');
const error = new Error('Generic error');
try {
handleBuildRequestError(error, platform);
} catch {}
expect(logDebugSpy).toBeCalledWith(undefined);
});
});
describe('error is server-side defined error', () => {
describe('throws correct EasCommandError subclass', () => {
describe('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
}
});
});
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
}
});
});
it('does it for EAS_BUILD_FREE_TIER_DISABLED_IOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_IOS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledIOSError);
}
});
it('does it for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledAndroidError);
}
});
describe('for VALIDATION_ERROR', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, RequestValidationError);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, RequestValidationError);
}
});
});
});
});
describe('error is other expected graphQL-related error', () => {
describe('throws correct EasCommandError subclass', () => {
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildDownForMaintenanceError,
EXPECTED_EAS_BUILD_DOWN_MESSAGE
);
}
});
});
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(
caughtError as Error,
EasBuildTooManyPendingBuildsError,
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
);
}
});
});
});
});
describe('error is unexpected graphQL-related error', () => {
describe('throws base Error class with custom message', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
}
});
});
});
describe('error is unexpected non-graphQL-related error', () => {
describe('throws base Error class with propagated message', () => {
it('does it with Android', async () => {
const platform = Platform.ANDROID;
const error = new Error('Non-graphQL-related error');
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
expect(caughtError).toStrictEqual(error);
}
});
it('does it with iOS', async () => {
const platform = Platform.IOS;
const error = new Error('Non-graphQL-related error');
try {
handleBuildRequestError(error, platform);
} catch (caughtError) {
expect(caughtError).toStrictEqual(error);
}
});
});
});
});
});
@@ -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();
});
});
});
@@ -73,39 +73,39 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.VERSION_CODE, bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(124); expect(fakeExp.android?.versionCode).toBe(124);
expect(appJSON.expo.version).toBe('3.0.0'); expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(124); expect(appJSON.expo.android.versionCode).toBe(124);
const buildGradle = await getAppBuildGradleAsync('/app'); const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle); assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124'); expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('124');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0'); 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 nativeVersionName = '1.0.0'; // this should be overwritten by bumpVersionAsync
const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName }); const fakeExp = initBareWorkflowProject({ versionName: nativeVersionName });
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION, bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('3.0.1');
expect(fakeExp.android?.versionCode).toBe(123); expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.1'); expect(appJSON.expo.version).toBe('3.0.1');
expect(appJSON.expo.android.versionCode).toBe(123); expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/app'); const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle); assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123'); expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1'); expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.1');
@@ -116,17 +116,17 @@ describe(bumpVersionAsync, () => {
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP, bumpStrategy: BumpStrategy.NOOP,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('3.0.0');
expect(fakeExp.android?.versionCode).toBe(123); expect(fakeExp.android?.versionCode).toBe(123);
expect(appJSON.expo.version).toBe('3.0.0'); expect(appJSON.expo.version).toBe('3.0.0');
expect(appJSON.expo.android.versionCode).toBe(123); expect(appJSON.expo.android.versionCode).toBe(123);
const buildGradle = await getAppBuildGradleAsync('/app'); const buildGradle = await getAppBuildGradleAsync('/repo');
assert(buildGradle); assert(buildGradle);
expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123'); expect(resolveConfigValue(buildGradle, 'versionCode')).toBe('123');
expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0'); expect(resolveConfigValue(buildGradle, 'versionName')).toBe('3.0.0');
@@ -140,11 +140,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({ await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.VERSION_CODE, bumpStrategy: BumpStrategy.VERSION_CODE,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(127); expect(fakeExp.android?.versionCode).toBe(127);
expect(appJSON.expo.version).toBe('5.0.0'); expect(appJSON.expo.version).toBe('5.0.0');
@@ -156,11 +156,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({ await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.APP_VERSION, bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('5.0.1');
expect(fakeExp.android?.versionCode).toBe(126); expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.1'); expect(appJSON.expo.version).toBe('5.0.1');
@@ -172,11 +172,11 @@ describe(bumpVersionInAppJsonAsync, () => {
await bumpVersionInAppJsonAsync({ await bumpVersionInAppJsonAsync({
bumpStrategy: BumpStrategy.NOOP, bumpStrategy: BumpStrategy.NOOP,
projectDir: '/app', projectDir: '/repo',
exp: fakeExp, 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.version).toBe('5.0.0');
expect(fakeExp.android?.versionCode).toBe(126); expect(fakeExp.android?.versionCode).toBe(126);
expect(appJSON.expo.version).toBe('5.0.0'); expect(appJSON.expo.version).toBe('5.0.0');
@@ -189,7 +189,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from native code', async () => { it('reads the versions from native code', async () => {
const exp = initBareWorkflowProject(); const exp = initBareWorkflowProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync( const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app', '/repo',
exp, exp,
{} as BuildProfile<Platform.ANDROID> {} as BuildProfile<Platform.ANDROID>
); );
@@ -201,7 +201,7 @@ describe(maybeResolveVersionsAsync, () => {
it('reads the versions from expo config', async () => { it('reads the versions from expo config', async () => {
const exp = initManagedProject(); const exp = initManagedProject();
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync( const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app', '/repo',
exp, exp,
{} as BuildProfile<Platform.ANDROID> {} as BuildProfile<Platform.ANDROID>
); );
@@ -240,7 +240,7 @@ function initBareWorkflowProject({
}`, }`,
'./android/app/src/main/AndroidManifest.xml': 'fake', './android/app/src/main/AndroidManifest.xml': 'fake',
}, },
'/app' '/repo'
); );
return fakeExp; return fakeExp;
@@ -261,7 +261,7 @@ function initManagedProject(): ExpoConfig {
expo: fakeExp, expo: fakeExp,
}), }),
}, },
'/app' '/repo'
); );
return fakeExp; return fakeExp;
+11 -35
View File
@@ -1,12 +1,10 @@
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job'; import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import AndroidCredentialsProvider, { import AndroidCredentialsProvider, {
AndroidCredentials, AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider'; } from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation'; import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log from '../../log'; import Log from '../../log';
import { import {
@@ -15,6 +13,7 @@ import {
} from '../../project/android/applicationId'; } from '../../project/android/applicationId';
import { resolveGradleBuildContextAsync } from '../../project/android/gradle'; import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
import { toggleConfirmAsync } from '../../prompts'; import { toggleConfirmAsync } from '../../prompts';
import { findAccountByName } from '../../user/Account';
import { import {
BuildRequestSender, BuildRequestSender,
CredentialsResult, CredentialsResult,
@@ -24,15 +23,10 @@ import {
import { AndroidBuildContext, BuildContext, CommonContext } from '../context'; import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
import { transformMetadata } from '../graphql'; import { transformMetadata } from '../graphql';
import { logCredentialsSource } from '../utils/credentials'; import { logCredentialsSource } from '../utils/credentials';
import { import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { transformJob } from './graphql'; import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob'; import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration'; import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
export async function createAndroidContextAsync( export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID> ctx: CommonContext<Platform.ANDROID>
@@ -50,34 +44,23 @@ This means that it will most likely produce an AAB and you will not be able to i
Log.newLine(); Log.newLine();
const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' }); const confirmed = await toggleConfirmAsync({ message: 'Would you like to proceed?' });
if (!confirmed) { if (!confirmed) {
Log.error('Update eas.json and come back again.'); Log.error('Please update eas.json and come back again.');
process.exit(1); process.exit(1);
} }
} }
checkNodeEnvVariable(ctx); checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx); await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile); const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) { if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx); await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
} }
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext); 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 }; return { applicationId, gradleContext };
} }
export async function prepareAndroidBuildAsync( export async function prepareAndroidBuildAsync(
@@ -89,14 +72,10 @@ export async function prepareAndroidBuildAsync(
return await ensureAndroidCredentialsAsync(ctx); return await ensureAndroidCredentialsAsync(ctx);
}, },
syncProjectConfigurationAsync: async () => { syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync(ctx.graphqlClient, { await syncProjectConfigurationAsync({
projectDir: ctx.projectDir, projectDir: ctx.projectDir,
exp: ctx.exp, exp: ctx.exp,
localAutoIncrement: buildProfile: ctx.buildProfile,
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
@@ -108,24 +87,21 @@ export async function prepareAndroidBuildAsync(
sendBuildRequestAsync: async ( sendBuildRequestAsync: async (
appId: string, appId: string,
job: Android.Job, job: Android.Job,
metadata: Metadata, metadata: Metadata
buildParams: BuildParamsInput
): Promise<BuildResult> => { ): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata); const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job); const graphqlJob = transformJob(job);
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, { return await BuildMutation.createAndroidBuildAsync({
appId, appId,
job: graphqlJob, job: graphqlJob,
metadata: graphqlMetadata, metadata: graphqlMetadata,
buildParams,
}); });
}, },
}); });
} }
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean { function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
const isCustomBuild = ctx.buildProfile.config !== undefined; return !ctx.buildProfile.withoutCredentials;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
} }
async function ensureAndroidCredentialsAsync( async function ensureAndroidCredentialsAsync(
@@ -142,7 +118,7 @@ async function ensureAndroidCredentialsAsync(
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, { const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
app: { app: {
account: nullthrows( 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}` `You do not have access to account: ${ctx.accountName}`
), ),
projectName: ctx.projectName, projectName: ctx.projectName,
+2 -12
View File
@@ -1,17 +1,11 @@
import { Android } from '@expo/eas-build-job'; import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated'; import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { import { transformProjectArchive, transformWorkflow } from '../graphql';
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Android.Job): AndroidJobInput { export function transformJob(job: Android.Job): AndroidJobInput {
return { return {
type: transformWorkflow(job.type), type: transformWorkflow(job.type),
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive), projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory, projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel, releaseChannel: job.releaseChannel,
@@ -19,16 +13,12 @@ export function transformJob(job: Android.Job): AndroidJobInput {
secrets: job.secrets, secrets: job.secrets,
builderEnvironment: job.builderEnvironment, builderEnvironment: job.builderEnvironment,
cache: job.cache, cache: job.cache,
version: job.version?.versionCode ? { versionCode: job.version.versionCode } : undefined,
gradleCommand: job.gradleCommand, gradleCommand: job.gradleCommand,
applicationArchivePath: job.applicationArchivePath, artifactPath: job.artifactPath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username, username: job.username,
buildType: job.buildType && transformBuildType(job.buildType), buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient, developmentClient: job.developmentClient,
experimental: job.experimental, experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
}; };
} }
@@ -1,19 +1,11 @@
import { import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
Android,
ArchiveSource,
BuildMode,
BuildTrigger,
Job,
Platform,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import path from 'path'; import path from 'path';
import slash from 'slash'; import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider'; import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs'; import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -25,13 +17,14 @@ interface JobData {
const cacheDefaults = { const cacheDefaults = {
disabled: false, disabled: false,
customPaths: [], customPaths: [],
cacheDefaultPaths: true,
}; };
export async function prepareJobAsync( export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>, ctx: BuildContext<Platform.ANDROID>,
jobData: JobData jobData: JobData
): Promise<Job> { ): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user); const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile; const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory = const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.'; slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
@@ -49,15 +42,11 @@ export async function prepareJobAsync(
} }
: {}; : {};
let buildType = buildProfile.buildType; let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') { if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK; buildType = Android.BuildType.APK;
} }
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Android.Job = { const job: Android.Job = {
type: ctx.workflow, type: ctx.workflow,
platform: Platform.ANDROID, platform: Platform.ANDROID,
@@ -79,29 +68,16 @@ export async function prepareJobAsync(
secrets: { secrets: {
...buildCredentials, ...buildCredentials,
}, },
releaseChannel: buildProfile.releaseChannel, releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: buildProfile.channel }, updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient, developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand, gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath, artifactPath: buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
buildType, buildType,
username, username,
...(ctx.android.versionCodeOverride && {
version: {
versionCode: ctx.android.versionCodeOverride,
},
}),
experimental: { experimental: {
prebuildCommand: buildProfile.prebuildCommand, prebuildCommand: ctx.buildProfile.prebuildCommand,
}, },
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
}; };
return sanitizeJob(job); return sanitizeJob(job);
@@ -1,39 +1,34 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins'; import { AndroidConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement } from '@expo/eas-json'; import { AndroidVersionAutoIncrement, BuildProfile } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log'; import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils'; import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule'; import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version'; import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync( export async function syncProjectConfigurationAsync({
graphqlClient: ExpoGraphqlClient, projectDir,
{ exp,
projectDir, buildProfile,
exp, }: {
localAutoIncrement, projectDir: string;
projectId, exp: ExpoConfig;
}: { buildProfile: BuildProfile<Platform.ANDROID>;
projectDir: string; }): Promise<void> {
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir); await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId); await syncUpdatesConfigurationAsync(projectDir, exp);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else { } else {
+12 -129
View File
@@ -1,22 +1,16 @@
import { ExpoConfig } from '@expo/config'; 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 { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; 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 Log from '../../log';
import { ora } from '../../ora';
import { import {
getAppBuildGradleAsync, getAppBuildGradleAsync,
parseGradleCommand, parseGradleCommand,
resolveConfigValue, resolveConfigValue,
} from '../../project/android/gradleUtils'; } from '../../project/android/gradleUtils';
import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import { updateAppJsonConfigAsync } from '../utils/appJson'; import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version'; import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
@@ -52,10 +46,9 @@ export async function bumpVersionAsync({
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp }); await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json'); Log.log('Updated versions in app.json');
await updateNativeVersionsAsync({ await writeVersionsToBuildGradleAsync({
projectDir, projectDir,
version: exp.version, exp,
versionCode: exp.android?.versionCode,
}); });
Log.log('Synchronized versions with build gradle'); Log.log('Synchronized versions with build gradle');
} }
@@ -81,7 +74,7 @@ export async function bumpVersionInAppJsonAsync({
await bumpAppVersionAsync({ appVersion, projectDir, exp }); await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else { } else {
const versionCode = AndroidConfig.Version.getVersionCode(exp); const versionCode = AndroidConfig.Version.getVersionCode(exp);
const bumpedVersionCode = getNextVersionCode(versionCode); const bumpedVersionCode = versionCode + 1;
Log.log( Log.log(
`Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold( `Bumping ${chalk.bold('expo.android.versionCode')} from ${chalk.bold(
versionCode versionCode
@@ -112,7 +105,7 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1', resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
}; };
} catch { } catch (err: any) {
return {}; return {};
} }
} else { } else {
@@ -123,33 +116,21 @@ export async function maybeResolveVersionsAsync(
} }
} }
export async function updateNativeVersionsAsync({ async function writeVersionsToBuildGradleAsync({
projectDir, projectDir,
version, exp,
versionCode,
}: { }: {
projectDir: string; projectDir: string;
version?: string; exp: ExpoConfig;
versionCode?: number; }): Promise<string> {
}): Promise<void> {
const buildGradle = await readBuildGradleAsync(projectDir); const buildGradle = await readBuildGradleAsync(projectDir);
if (!buildGradle) { if (!buildGradle) {
throw new Error('This project is missing a build.gradle file.'); throw new Error('This project is missing a build.gradle file.');
} }
let updatedBuildGradle = buildGradle; let updatedBuildGradle = AndroidConfig.Version.setVersionName(exp, buildGradle);
if (version !== undefined) { updatedBuildGradle = AndroidConfig.Version.setVersionCode(exp, updatedBuildGradle);
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionName ".*"`),
`versionName "${version}"`
);
}
if (versionCode !== undefined) {
updatedBuildGradle = updatedBuildGradle.replace(
new RegExp(`versionCode.*`),
`versionCode ${versionCode}`
);
}
await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle }); await writeBuildGradleAsync({ projectDir, buildGradle: updatedBuildGradle });
return updatedBuildGradle;
} }
async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> { async function readBuildGradleAsync(projectDir: string): Promise<string | undefined> {
@@ -170,101 +151,3 @@ async function writeBuildGradleAsync({
const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir); const buildGradlePath = AndroidConfig.Paths.getAppBuildGradleFilePath(projectDir);
await fs.writeFile(buildGradlePath, buildGradle); 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);
}
}
+91 -259
View File
@@ -1,53 +1,36 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job'; import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json'; import { CredentialsSource } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import cliProgress from 'cli-progress'; import cliProgress from 'cli-progress';
import fs from 'fs-extra'; import fs from 'fs-extra';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common'; import { withAnalyticsAsync } from '../analytics/common';
import { BuildEvent } from '../analytics/events';
import { getExpoWebsiteBaseUrl } from '../api'; import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import { import {
AppPlatform, AppPlatform,
BuildFragment, BuildFragment,
BuildParamsInput,
BuildPriority, BuildPriority,
BuildResourceClass,
BuildStatus, BuildStatus,
UploadSessionType, UploadSessionType,
} from '../graphql/generated'; } from '../graphql/generated';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation'; import { BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery'; import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log'; import Log, { learnMore } from '../log';
import { Ora, ora } from '../ora'; import { Ora, ora } from '../ora';
import { import {
appPlatformDisplayNames, appPlatformDisplayNames,
appPlatformEmojis, appPlatformEmojis,
requestedPlatformDisplayNames, requestedPlatformDisplayNames,
} from '../platform'; } from '../platform';
import { confirmAsync } from '../prompts'; import { uploadAsync } from '../uploads';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files'; import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress'; import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise'; import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
import { BuildContext } from './context'; import { BuildContext } from './context';
import { import { runLocalBuildAsync } from './local';
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from './errors';
import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata'; import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo'; import { printDeprecationWarnings } from './utils/printBuildInfo';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository'; import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
@@ -70,22 +53,11 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
): Promise<CredentialsResult<Credentials> | undefined>; ): Promise<CredentialsResult<Credentials> | undefined>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>; syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>; prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync( sendBuildRequestAsync(appId: string, job: TJob, metadata: Metadata): Promise<BuildResult>;
appId: string,
job: TJob,
metadata: Metadata,
buildParams: BuildParamsInput
): Promise<BuildResult>;
} }
export type BuildRequestSender = () => Promise<BuildFragment | undefined>; export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
return {
resourceClass: ctx.resourceClass,
};
}
export async function prepareBuildRequestForPlatformAsync< export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform, TPlatform extends Platform,
Credentials, Credentials,
@@ -93,26 +65,21 @@ export async function prepareBuildRequestForPlatformAsync<
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> { >(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder; const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync( const credentialsResult = await withAnalyticsAsync(
ctx.analytics,
async () => await builder.ensureCredentialsAsync(ctx), async () => await builder.ensureCredentialsAsync(ctx),
{ {
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT, attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS, successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL, failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
properties: ctx.analyticsEventProperties, trackingCtx: ctx.trackingCtx,
} }
); );
await withAnalyticsAsync( await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
ctx.analytics, attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
async () => await builder.syncProjectConfigurationAsync(ctx), successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
{ failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT, trackingCtx: ctx.trackingCtx,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS, });
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
properties: ctx.analyticsEventProperties,
}
);
if (await getVcsClient().isCommitRequiredAsync()) { if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone(); Log.addNewLineIfNone();
@@ -122,83 +89,62 @@ export async function prepareBuildRequestForPlatformAsync<
); );
} }
let projectArchive: ArchiveSource | undefined; const projectArchive = ctx.localBuildOptions.enable
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) { ? ({
projectArchive = { type: ArchiveSourceType.PATH,
type: ArchiveSourceType.PATH, path: (await makeProjectTarballAsync()).path,
path: (await makeProjectTarballAsync()).path, } as const)
}; : ({
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) { type: ArchiveSourceType.S3,
projectArchive = { bucketKey: await uploadProjectAsync(ctx),
type: ArchiveSourceType.PATH, } as const);
path: process.cwd(),
};
} else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = {
type: ArchiveSourceType.GCS,
bucketKey: await uploadProjectAsync(ctx),
};
}
assert(projectArchive);
const metadata = await collectMetadataAsync(ctx); const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx);
const job = await builder.prepareJobAsync(ctx, { const job = await builder.prepareJobAsync(ctx, {
projectArchive, projectArchive,
credentials: credentialsResult?.credentials, credentials: credentialsResult?.credentials,
}); });
return async () => { return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) { if (ctx.localBuildOptions.enable) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions); await runLocalBuildAsync(job, ctx.localBuildOptions);
return undefined; return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) { } else {
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 { try {
return await sendBuildRequestAsync(builder, job, metadata, buildParams); return await sendBuildRequestAsync(builder, job, metadata);
} catch (error: any) { } catch (error: any) {
handleBuildRequestError(error, job.platform); handleBuildRequestError(error, job.platform);
} }
} else {
throw new Error('Unknown localBuildMode.');
} }
}; };
} }
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = { function handleBuildRequestError(error: any, platform: Platform): never {
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError, if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError, Log.error('EAS Build API has changed, please upgrade to the latest eas-cli version.');
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError, throw new Error('Build request failed.');
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError, } else if (
VALIDATION_ERROR: RequestValidationError, error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
}; ) {
Log.error(
export function handleBuildRequestError(error: any, platform: Platform): never { 'EAS Build is down for maintenance, please try again later. Check https://status.expo.dev/ for updates.'
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
throw new EasBuildDownForMaintenanceError(
`EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`
); );
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') { throw new Error('Build request failed.');
throw new EasBuildTooManyPendingBuildsError( } 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[platform]} builds for your account. Try again later.` `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) { } else if (error?.graphQLErrors) {
throw new Error( Log.error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.' 'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, please report the issue.'
); );
} }
throw error; throw error;
@@ -210,31 +156,12 @@ async function uploadProjectAsync<TPlatform extends Platform>(
let projectTarballPath; let projectTarballPath;
try { try {
return await withAnalyticsAsync( return await withAnalyticsAsync(
ctx.analytics,
async () => { async () => {
Log.newLine();
Log.log(
`Compressing project files and uploading to EAS Build. ${learnMore(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync(); 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; projectTarballPath = projectTarball.path;
const bucketKey = await uploadFileAtPathToGCSAsync( const { bucketKey } = await uploadAsync(
ctx.graphqlClient, UploadSessionType.EasBuildProjectSources,
UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path, projectTarball.path,
createProgressTracker({ createProgressTracker({
total: projectTarball.size, total: projectTarball.size,
@@ -242,7 +169,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes( `Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size 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; return bucketKey;
@@ -251,7 +181,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT, attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS, successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL, failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
properties: ctx.analyticsEventProperties, trackingCtx: ctx.trackingCtx,
} }
); );
} finally { } finally {
@@ -264,12 +194,10 @@ async function uploadProjectAsync<TPlatform extends Platform>(
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>( async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
builder: Builder<TPlatform, Credentials, TJob>, builder: Builder<TPlatform, Credentials, TJob>,
job: TJob, job: TJob,
metadata: Metadata, metadata: Metadata
buildParams: BuildParamsInput
): Promise<BuildFragment> { ): Promise<BuildFragment> {
const { ctx } = builder; const { ctx } = builder;
return await withAnalyticsAsync( return await withAnalyticsAsync(
ctx.analytics,
async () => { async () => {
if (Log.isDebug) { if (Log.isDebug) {
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`); Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
@@ -278,8 +206,7 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
const { build, deprecationInfo } = await builder.sendBuildRequestAsync( const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
ctx.projectId, ctx.projectId,
job, job,
metadata, metadata
buildParams
); );
printDeprecationWarnings(deprecationInfo); printDeprecationWarnings(deprecationInfo);
@@ -289,56 +216,46 @@ async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJ
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT, attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS, successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
failureEvent: BuildEvent.BUILD_REQUEST_FAIL, failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
properties: ctx.analyticsEventProperties, trackingCtx: ctx.trackingCtx,
} }
); );
} }
export type MaybeBuildFragment = BuildFragment | null; type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync( export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient, { buildIds, accountName }: { buildIds: string[]; accountName: string },
{ {
buildIds, // 2 hours (max build time limit) + 10 minutes (possible queue time)
accountName, timeoutSec = 2 * 60 * 60 + 10 * 60,
projectDir, intervalSec = 10,
nonInteractive, } = {}
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> { ): Promise<MaybeBuildFragment[]> {
let spinner; const b = `build${buildIds.length > 1 ? 's' : ''}`;
let originalSpinnerText; Log.log(`Waiting for ${b} to complete. You can press Ctrl+C to exit.`);
if (buildIds.length === 1) { const spinner = ora(`Waiting for ${b} to complete.`).start();
Log.log('Waiting for build to complete. You can press Ctrl+C to exit.'); const endTime = new Date().getTime() + timeoutSec * 1000;
originalSpinnerText = 'Waiting for build to complete.'; while (new Date().getTime() <= endTime) {
spinner = ora(originalSpinnerText).start(); const builds = await getBuildsSafelyAsync(buildIds);
} 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 } = const { refetch } =
builds.length === 1 builds.length === 1
? await handleSingleBuildProgressAsync( ? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
{ build: builds[0], accountName, projectDir, nonInteractive }, : await handleMultipleBuildsProgressAsync({ builds }, { spinner });
{ spinner }
)
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) { if (!refetch) {
return builds; return builds;
} }
await sleepAsync(intervalSec * 1000); await sleepAsync(intervalSec * 1000);
} }
spinner.fail('Timed out');
throw new Error(
'Timeout reached! It is taking longer than expected to finish the build, aborting...'
);
} }
async function getBuildsSafelyAsync( async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
graphqlClient: ExpoGraphqlClient,
buildIds: string[]
): Promise<MaybeBuildFragment[]> {
const promises = buildIds.map(async buildId => { const promises = buildIds.map(async buildId => {
try { try {
return await BuildQuery.byIdAsync(graphqlClient, buildId, { useCache: false }); return await BuildQuery.byIdAsync(buildId, { useCache: false });
} catch (err) { } catch (err) {
Log.debug('Failed to fetch the build status', err); Log.debug('Failed to fetch the build status', err);
return null; return null;
@@ -358,17 +275,7 @@ const queueProgressBar = new cliProgress.SingleBar(
); );
async function handleSingleBuildProgressAsync( async function handleSingleBuildProgressAsync(
{ { build, accountName }: { build: MaybeBuildFragment; accountName: string },
build,
accountName,
projectDir,
nonInteractive,
}: {
build: MaybeBuildFragment;
accountName: string;
projectDir: string;
nonInteractive: boolean;
},
{ spinner }: { spinner: Ora } { spinner }: { spinner: Ora }
): Promise<BuildProgressResult> { ): Promise<BuildProgressResult> {
if (build === null) { if (build === null) {
@@ -393,7 +300,7 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished'); spinner.succeed('Build finished');
return { refetch: false }; return { refetch: false };
case BuildStatus.New: case BuildStatus.New:
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link( spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName) formatAccountSubscriptionsUrl(accountName)
)}.`; )}.`;
break; break;
@@ -407,70 +314,20 @@ async function handleSingleBuildProgressAsync(
if ( if (
!queueProgressBarStarted && !queueProgressBarStarted &&
typeof build.initialQueuePosition === 'number' && typeof build.initialQueuePosition === 'number' &&
typeof build.queuePosition === 'number' && typeof build.queuePosition === 'number'
typeof build.estimatedWaitTimeLeftSeconds === 'number'
) { ) {
spinner.stopAndPersist(); spinner.stopAndPersist();
if (build.priority !== BuildPriority.High) { if (build.priority !== BuildPriority.High) {
Log.newLine(); Log.newLine();
Log.log('Start builds sooner in the priority queue.'); Log.log('Start builds sooner in the priority queue.');
Log.log( Log.log(
`Sign up for EAS Production or Enterprise at ${link( `Sign up for EAS Production or Enterprise at ${chalk.underline(
formatAccountSubscriptionsUrl(accountName) 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.newLine();
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`); Log.log('Waiting in queue');
queueProgressBar.start( queueProgressBar.start(
build.initialQueuePosition + 1, build.initialQueuePosition + 1,
build.initialQueuePosition - build.queuePosition + 1, build.initialQueuePosition - build.queuePosition + 1,
@@ -503,12 +360,6 @@ async function handleSingleBuildProgressAsync(
return { refetch: true }; 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> = { const statusToDisplayName: Record<BuildStatus, string> = {
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)', [BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
[BuildStatus.InQueue]: 'in queue', [BuildStatus.InQueue]: 'in queue',
@@ -522,7 +373,7 @@ const platforms = [AppPlatform.Android, AppPlatform.Ios];
async function handleMultipleBuildsProgressAsync( async function handleMultipleBuildsProgressAsync(
{ builds: maybeBuilds }: { builds: MaybeBuildFragment[] }, { builds: maybeBuilds }: { builds: MaybeBuildFragment[] },
{ spinner, originalSpinnerText }: { spinner: Ora; originalSpinnerText: string } { spinner }: { spinner: Ora }
): Promise<BuildProgressResult> { ): Promise<BuildProgressResult> {
const buildCount = maybeBuilds.length; const buildCount = maybeBuilds.length;
const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment); const builds = maybeBuilds.filter<BuildFragment>(isBuildFragment);
@@ -542,7 +393,7 @@ async function handleMultipleBuildsProgressAsync(
} }
return { refetch: false }; return { refetch: false };
} else { } else {
spinner.text = formatPendingBuildsText(originalSpinnerText, builds); spinner.text = formatPendingBuildsText(builds);
return { refetch: true }; return { refetch: true };
} }
} }
@@ -561,10 +412,9 @@ function formatSettledBuildsText(builds: BuildFragment[]): string {
.join('\n '); .join('\n ');
} }
function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragment[]): string { function formatPendingBuildsText(builds: BuildFragment[]): string {
return [ return platforms
originalSpinnerText, .map(platform => {
...platforms.map(platform => {
const build = builds.find(build => build.platform === platform); const build = builds.find(build => build.platform === platform);
const status = build ? statusToDisplayName[build.status] : 'unknown'; const status = build ? statusToDisplayName[build.status] : 'unknown';
let extraInfo = ''; let extraInfo = '';
@@ -587,8 +437,8 @@ function formatPendingBuildsText(originalSpinnerText: string, builds: BuildFragm
return `${appPlatformEmojis[platform]} ${ return `${appPlatformEmojis[platform]} ${
appPlatformDisplayNames[platform] appPlatformDisplayNames[platform]
} build - status: ${chalk.bold(status)}${extraInfo}`; } build - status: ${chalk.bold(status)}${extraInfo}`;
}), })
].join('\n '); .join('\n ');
} }
function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment { function isBuildFragment(maybeBuild: MaybeBuildFragment): maybeBuild is BuildFragment {
@@ -610,21 +460,3 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
getExpoWebsiteBaseUrl() getExpoWebsiteBaseUrl()
).toString(); ).toString();
} }
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(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.M_MEDIUM,
};
}
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
}
+5 -23
View File
@@ -1,5 +1,5 @@
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor, ResourceClass } from '@expo/eas-json'; import { EasJson, EasJsonReader } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
@@ -24,7 +24,7 @@ interface ConfigureParams {
export async function ensureProjectConfiguredAsync( export async function ensureProjectConfiguredAsync(
configureParams: ConfigureParams configureParams: ConfigureParams
): Promise<boolean> { ): Promise<boolean> {
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) { if (await fs.pathExists(EasJsonReader.formatEasJsonPath(configureParams.projectDir))) {
return false; return false;
} }
@@ -53,21 +53,11 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
development: { development: {
developmentClient: true, developmentClient: true,
distribution: 'internal', distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
}, },
preview: { preview: {
distribution: 'internal', distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
}, },
production: {},
}, },
submit: { submit: {
production: {}, production: {},
@@ -86,20 +76,12 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
}, },
ios: { ios: {
buildConfiguration: 'Debug', buildConfiguration: 'Debug',
resourceClass: ResourceClass.M_MEDIUM,
}, },
}, },
preview: { preview: {
distribution: 'internal', distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
},
}, },
production: {},
}, },
submit: { submit: {
production: {}, production: {},
@@ -107,7 +89,7 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
}; };
async function createEasJsonAsync(projectDir: string): Promise<void> { async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir); const easJsonPath = EasJsonReader.formatEasJsonPath(projectDir);
const hasAndroidNativeProject = const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC; (await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
+5 -14
View File
@@ -1,12 +1,11 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager'; import { TrackingContext } from '../analytics/common';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context'; import { CredentialsContext } from '../credentials/context';
import { Target } from '../credentials/ios/types'; import { Target } from '../credentials/ios/types';
import { BuildResourceClass } from '../graphql/generated';
import { GradleBuildContext } from '../project/android/gradle'; import { GradleBuildContext } from '../project/android/gradle';
import { XcodeBuildContext } from '../project/ios/scheme'; import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
@@ -17,39 +16,31 @@ export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android'
export interface AndroidBuildContext { export interface AndroidBuildContext {
applicationId: string; applicationId: string;
gradleContext?: GradleBuildContext; gradleContext?: GradleBuildContext;
versionCodeOverride?: string;
} }
export interface IosBuildContext { export interface IosBuildContext {
bundleIdentifier: string; bundleIdentifier: string;
applicationTargetBuildSettings: XCBuildConfiguration['buildSettings'];
applicationTarget: Target; applicationTarget: Target;
targets: Target[]; targets: Target[];
xcodeBuildContext: XcodeBuildContext; xcodeBuildContext: XcodeBuildContext;
buildNumberOverride?: string;
} }
export interface BuildContext<T extends Platform> { export interface BuildContext<T extends Platform> {
accountName: string; accountName: string;
easJsonCliConfig: EasJson['cli'];
buildProfile: BuildProfile<T>; buildProfile: BuildProfile<T>;
buildProfileName: string; buildProfileName: string;
resourceClass: BuildResourceClass;
clearCache: boolean; clearCache: boolean;
credentialsCtx: CredentialsContext; credentialsCtx: CredentialsContext;
exp: ExpoConfig; exp: ExpoConfig;
localBuildOptions: LocalBuildOptions; localBuildOptions: LocalBuildOptions;
nonInteractive: boolean; nonInteractive: boolean;
noWait: boolean;
runFromCI: boolean;
platform: T; platform: T;
projectDir: string; projectDir: string;
projectId: string; projectId: string;
projectName: string; projectName: string;
message?: string; trackingCtx: TrackingContext;
analyticsEventProperties: AnalyticsEventProperties;
user: Actor; user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
workflow: Workflow; workflow: Workflow;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined; android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined; ios: T extends Platform.IOS ? IosBuildContext : undefined;
+22 -58
View File
@@ -1,72 +1,54 @@
import { Platform } from '@expo/eas-build-job'; import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import JsonFile from '@expo/json-file'; import JsonFile from '@expo/json-file';
import getenv from 'getenv';
import resolveFrom from 'resolve-from'; import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager'; import { TrackingContext } from '../analytics/common';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField'; import { Analytics, BuildEvent } from '../analytics/events';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context'; import { CredentialsContext } from '../credentials/context';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils'; import { getExpoConfig } from '../project/expoConfig';
import { getProjectAccountName, getProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow'; import { resolveWorkflowAsync } from '../project/workflow';
import { Actor } from '../user/User'; import { findAccountByName } from '../user/Account';
import { ensureLoggedInAsync } from '../user/actions';
import { createAndroidContextAsync } from './android/build'; import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context'; import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build'; import { createIosContextAsync } from './ios/build';
import { LocalBuildOptions } from './local'; import { LocalBuildOptions } from './local';
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
export async function createBuildContextAsync<T extends Platform>({ export async function createBuildContextAsync<T extends Platform>({
buildProfileName, buildProfileName,
buildProfile, buildProfile,
easJsonCliConfig,
clearCache = false, clearCache = false,
localBuildOptions, localBuildOptions,
nonInteractive, nonInteractive = false,
noWait,
platform, platform,
projectDir, projectDir,
resourceClassFlag,
message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: { }: {
buildProfileName: string; buildProfileName: string;
buildProfile: BuildProfile<T>; buildProfile: BuildProfile<T>;
easJsonCliConfig: EasJson['cli'];
clearCache: boolean; clearCache: boolean;
localBuildOptions: LocalBuildOptions; localBuildOptions: LocalBuildOptions;
nonInteractive: boolean; nonInteractive: boolean;
noWait: boolean;
platform: T; platform: T;
projectDir: string; projectDir: string;
resourceClassFlag?: ResourceClass;
message?: string;
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
}): Promise<BuildContext<T>> { }): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env }); const exp = getExpoConfig(projectDir, { env: buildProfile.env });
const user = await ensureLoggedInAsync();
const accountName = getProjectAccountName(exp, user);
const projectName = exp.slug; const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); const projectId = await getProjectIdAsync(exp, { env: buildProfile.env });
const workflow = await resolveWorkflowAsync(projectDir, platform); const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = account.id; const accountId = findAccountByName(user.accounts, accountName)?.id;
const runFromCI = getenv.boolish('CI', false);
const credentialsCtx = new CredentialsContext({ const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId }, exp,
nonInteractive, nonInteractive,
projectDir, projectDir,
user: actor, user,
graphqlClient,
analytics,
env: buildProfile.env, env: buildProfile.env,
easJsonCliConfig,
}); });
const devClientProperties = getDevClientEventProperties({ const devClientProperties = getDevClientEventProperties({
@@ -74,50 +56,32 @@ export async function createBuildContextAsync<T extends Platform>({
projectDir, projectDir,
buildProfile, buildProfile,
}); });
const analyticsEventProperties = { const trackingCtx = {
tracking_id: uuidv4(), tracking_id: uuidv4(),
platform, platform,
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
...(accountId && { account_id: accountId }), ...(accountId && { account_id: accountId }),
project_id: projectId, project_id: projectId,
project_type: workflow, project_type: workflow,
...devClientProperties, ...devClientProperties,
no_wait: noWait,
run_from_ci: runFromCI,
}; };
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties); Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
const resourceClass = await resolveBuildResourceClassAsync(
buildProfile,
platform,
projectDir,
exp,
resourceClassFlag
);
const commonContext: CommonContext<T> = { const commonContext: CommonContext<T> = {
accountName: account.name, accountName,
buildProfile, buildProfile,
buildProfileName, buildProfileName,
resourceClass,
easJsonCliConfig,
clearCache, clearCache,
credentialsCtx, credentialsCtx,
exp, exp,
localBuildOptions, localBuildOptions,
nonInteractive, nonInteractive,
noWait,
platform, platform,
projectDir, projectDir,
projectId, projectId,
projectName, projectName,
analyticsEventProperties, trackingCtx,
user: actor, user,
graphqlClient,
analytics,
workflow, workflow,
message,
runFromCI,
}; };
if (platform === Platform.ANDROID) { if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>; const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -142,7 +106,7 @@ function getDevClientEventProperties({
platform: Platform; platform: Platform;
projectDir: string; projectDir: string;
buildProfile: BuildProfile; buildProfile: BuildProfile;
}): Partial<AnalyticsEventProperties> { }): Partial<TrackingContext> {
let includesDevClient; let includesDevClient;
const version = tryGetDevClientVersion(projectDir); const version = tryGetDevClientVersion(projectDir);
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) { if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
-15
View File
@@ -1,15 +0,0 @@
import { EasCommandError } from '../commandUtils/errors';
export class TurtleDeprecatedJobFormatError extends EasCommandError {}
export class EasBuildFreeTierDisabledError extends EasCommandError {}
export class EasBuildFreeTierDisabledIOSError extends EasCommandError {}
export class EasBuildFreeTierDisabledAndroidError extends EasCommandError {}
export class RequestValidationError extends EasCommandError {}
export class EasBuildDownForMaintenanceError extends EasCommandError {}
export class EasBuildTooManyPendingBuildsError extends EasCommandError {}
+1 -37
View File
@@ -1,19 +1,11 @@
import { import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
ArchiveSource,
ArchiveSourceType,
BuildTrigger,
Metadata,
Workflow,
} from '@expo/eas-build-job';
import { import {
BuildCredentialsSource, BuildCredentialsSource,
BuildIosEnterpriseProvisioning, BuildIosEnterpriseProvisioning,
BuildMetadataInput, BuildMetadataInput,
BuildMode,
BuildWorkflow, BuildWorkflow,
DistributionType, DistributionType,
BuildTrigger as GraphQLBuildTrigger,
ProjectArchiveSourceInput, ProjectArchiveSourceInput,
ProjectArchiveSourceType, ProjectArchiveSourceType,
} from '../graphql/generated'; } from '../graphql/generated';
@@ -24,11 +16,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
type: ProjectArchiveSourceType.S3, type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey, bucketKey: archiveSource.bucketKey,
}; };
} else if (archiveSource.type === ArchiveSourceType.GCS) {
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.URL) { } else if (archiveSource.type === ArchiveSourceType.URL) {
return { return {
type: ProjectArchiveSourceType.Url, type: ProjectArchiveSourceType.Url,
@@ -42,7 +29,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput { export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return { return {
...metadata, ...metadata,
buildMode: metadata.buildMode && transformBuildMode(metadata.buildMode),
credentialsSource: credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource), metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution), distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -90,25 +76,3 @@ export function transformIosEnterpriseProvisioning(
return BuildIosEnterpriseProvisioning.Universal; return BuildIosEnterpriseProvisioning.Universal;
} }
} }
// TODO: check what in metadata
export function transformBuildMode(buildMode: string): BuildMode {
if (buildMode === 'build') {
return BuildMode.Build;
} else if (buildMode === 'resign') {
return BuildMode.Resign;
} else if (buildMode === 'custom') {
return BuildMode.Custom;
} else {
throw new Error(`Unsupported build mode: ${buildMode}`);
}
}
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');
}
@@ -3,7 +3,6 @@ import { IOSConfig } from '@expo/config-plugins';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { vol } from 'memfs'; import { vol } from 'memfs';
import os from 'os'; import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { readPlistAsync } from '../../../utils/plist'; import { readPlistAsync } from '../../../utils/plist';
import { import {
@@ -18,12 +17,6 @@ import {
jest.mock('fs'); jest.mock('fs');
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target,
'getXCBuildConfigurationFromPbxproj'
);
const getPbxproj = jest.spyOn(IOSConfig.XcodeUtils, 'getPbxproj');
afterAll(async () => { afterAll(async () => {
// do not remove the following line // do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils // this fixes a weird error with tempy in @expo/image-utils
@@ -58,34 +51,14 @@ describe(evaluateTemplateString, () => {
// bare workflow // bare workflow
describe(bumpVersionAsync, () => { 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 () => { it('bumps expo.ios.buildNumber and CFBundleVersion when strategy = BumpStrategy.BUILD_NUMBER', async () => {
const fakeExp = initBareWorkflowProject(); const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER, bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo', projectDir: '/repo',
exp: fakeExp, exp: fakeExp,
targets: [ buildSettings: {},
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
}); });
const appJSON = await fs.readJSON('/repo/app.json'); 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 () => { it('bumps expo.ios.buildNumber and CFBundleVersion for non default Info.plist location', async () => {
const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' }); const fakeExp = initBareWorkflowProject({ infoPlistName: 'Info2.plist' });
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER, bumpStrategy: BumpStrategy.BUILD_NUMBER,
projectDir: '/repo', projectDir: '/repo',
exp: fakeExp, exp: fakeExp,
targets: [ buildSettings: {
{ INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
targetName: 'example', },
bundleIdentifier: 'example.com',
entitlements: {},
},
],
}); });
const appJSON = await fs.readJSON('/repo/app.json'); const appJSON = await fs.readJSON('/repo/app.json');
@@ -136,27 +97,14 @@ describe(bumpVersionAsync, () => {
expect(infoPlist['CFBundleVersion']).toBe('2'); 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(); const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION, bumpStrategy: BumpStrategy.APP_VERSION,
projectDir: '/repo', projectDir: '/repo',
exp: fakeExp, exp: fakeExp,
targets: [ buildSettings: {},
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
}); });
const appJSON = await fs.readJSON('/repo/app.json'); 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 () => { it('does not bump any version when strategy = BumpStrategy.NOOP', async () => {
const fakeExp = initBareWorkflowProject(); const fakeExp = initBareWorkflowProject();
getXCBuildConfigurationFromPbxproj.mockImplementation(
() =>
({
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
} as XCBuildConfiguration)
);
await bumpVersionAsync({ await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP, bumpStrategy: BumpStrategy.NOOP,
projectDir: '/repo', projectDir: '/repo',
exp: fakeExp, exp: fakeExp,
targets: [ buildSettings: {},
{
targetName: 'example',
bundleIdentifier: 'example.com',
entitlements: {},
},
],
}); });
const appJSON = await fs.readJSON('/repo/app.json'); const appJSON = await fs.readJSON('/repo/app.json');
@@ -353,9 +288,9 @@ describe(getInfoPlistPath, () => {
function initBareWorkflowProject({ function initBareWorkflowProject({
appVersion = '1.0.0', appVersion = '1.0.0',
buildNumber = '1', version = '1',
infoPlistName = 'Info.plist', infoPlistName = 'Info.plist',
}: { appVersion?: string; buildNumber?: string; infoPlistName?: string } = {}): ExpoConfig { }: { appVersion?: string; version?: string; infoPlistName?: string } = {}): ExpoConfig {
const fakeExp: ExpoConfig = { const fakeExp: ExpoConfig = {
name: 'myproject', name: 'myproject',
slug: 'myproject', slug: 'myproject',
@@ -376,7 +311,7 @@ function initBareWorkflowProject({
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>${appVersion}</string> <string>${appVersion}</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>${buildNumber}</string> <string>${version}</string>
</dict> </dict>
</plist>`, </plist>`,
}, },
+28 -33
View File
@@ -1,8 +1,8 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job'; 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 { IosCredentials, Target } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation'; import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier'; import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme'; import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
@@ -10,16 +10,11 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build'; import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
import { BuildContext, CommonContext, IosBuildContext } from '../context'; import { BuildContext, CommonContext, IosBuildContext } from '../context';
import { transformMetadata } from '../graphql'; import { transformMetadata } from '../graphql';
import { import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
checkGoogleServicesFileAsync,
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { ensureIosCredentialsAsync } from './credentials'; import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql'; import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob'; import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration'; import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteBuildNumberAsync } from './version';
export async function createIosContextAsync( export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS> ctx: CommonContext<Platform.IOS>
@@ -27,12 +22,11 @@ export async function createIosContextAsync(
const { buildProfile } = ctx; const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) { if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx); await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
} }
checkNodeEnvVariable(ctx); checkNodeEnvVariable(ctx);
await checkGoogleServicesFileAsync(ctx); await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const xcodeBuildContext = await resolveXcodeBuildContextAsync( const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{ {
@@ -49,22 +43,14 @@ export async function createIosContextAsync(
env: buildProfile.env, env: buildProfile.env,
}); });
const applicationTarget = findApplicationTarget(targets); const applicationTarget = findApplicationTarget(targets);
const buildNumberOverride = const applicationTargetBuildSettings = resolveBuildSettings(ctx, applicationTarget);
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteBuildNumberAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
projectId: ctx.projectId,
exp: ctx.exp,
applicationTarget,
buildProfile,
})
: undefined;
return { return {
bundleIdentifier: applicationTarget.bundleIdentifier, bundleIdentifier: applicationTarget.bundleIdentifier,
applicationTarget, applicationTarget,
applicationTargetBuildSettings,
targets, targets,
xcodeBuildContext, xcodeBuildContext,
buildNumberOverride,
}; };
} }
@@ -77,15 +63,11 @@ export async function prepareIosBuildAsync(
return ensureIosCredentialsAsync(ctx, ctx.ios.targets); return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
}, },
syncProjectConfigurationAsync: async () => { syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync(ctx.graphqlClient, { await syncProjectConfigurationAsync({
projectDir: ctx.projectDir, projectDir: ctx.projectDir,
exp: ctx.exp, exp: ctx.exp,
targets: ctx.ios.targets, buildProfile: ctx.buildProfile,
localAutoIncrement: buildSettings: ctx.ios.applicationTargetBuildSettings,
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
@@ -100,17 +82,30 @@ export async function prepareIosBuildAsync(
sendBuildRequestAsync: async ( sendBuildRequestAsync: async (
appId: string, appId: string,
job: Ios.Job, job: Ios.Job,
metadata: Metadata, metadata: Metadata
buildParams: BuildParamsInput
): Promise<BuildResult> => { ): Promise<BuildResult> => {
const graphqlMetadata = transformMetadata(metadata); const graphqlMetadata = transformMetadata(metadata);
const graphqlJob = transformJob(job); const graphqlJob = transformJob(job);
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, { return await BuildMutation.createIosBuildAsync({
appId, appId,
job: graphqlJob, job: graphqlJob,
metadata: graphqlMetadata, metadata: graphqlMetadata,
buildParams,
}); });
}, },
}); });
} }
function resolveBuildSettings(
ctx: CommonContext<Platform.IOS>,
applicationTarget: Target
): XCBuildConfiguration['buildSettings'] {
if (ctx.workflow === Workflow.MANAGED) {
return {};
}
const project = IOSConfig.XcodeUtils.getPbxproj(ctx.projectDir);
const xcBuildConfiguration = IOSConfig.Target.getXCBuildConfigurationFromPbxproj(project, {
targetName: applicationTarget.targetName,
buildConfiguration: applicationTarget.buildConfiguration,
});
return xcBuildConfiguration?.buildSettings ?? {};
}
+3 -27
View File
@@ -1,9 +1,7 @@
import { Platform } from '@expo/eas-build-job'; 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 IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils'; import { getAppFromContext } from '../../credentials/ios/actions/BuildCredentialsUtils';
import { IosCredentials, Target } from '../../credentials/ios/types'; import { IosCredentials, Target } from '../../credentials/ios/types';
import { CredentialsResult } from '../build'; import { CredentialsResult } from '../build';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -18,7 +16,7 @@ export async function ensureIosCredentialsAsync(
} }
const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, { const provider = new IosCredentialsProvider(buildCtx.credentialsCtx, {
app: await getAppFromContextAsync(buildCtx.credentialsCtx), app: getAppFromContext(buildCtx.credentialsCtx),
targets, targets,
distribution: buildCtx.buildProfile.distribution ?? 'store', distribution: buildCtx.buildProfile.distribution ?? 'store',
enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning, enterpriseProvisioning: buildCtx.buildProfile.enterpriseProvisioning,
@@ -33,28 +31,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 { function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
const isCustomBuild = buildCtx.buildProfile.config !== undefined; return !buildCtx.buildProfile.simulator;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
} }
+8 -16
View File
@@ -1,41 +1,32 @@
import { Ios } from '@expo/eas-build-job'; import { Env, Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated'; import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { import { transformProjectArchive, transformWorkflow } from '../graphql';
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput { export function transformJob(job: Ios.Job): IosJobInput {
return { return {
type: transformWorkflow(job.type), type: transformWorkflow(job.type),
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive), projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: nullthrows(job.projectRootDirectory), projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel, releaseChannel: job.releaseChannel,
updates: job.updates, updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined, secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment, builderEnvironment: job.builderEnvironment,
cache: job.cache, cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
scheme: job.scheme, scheme: job.scheme,
buildConfiguration: job.buildConfiguration, buildConfiguration: job.buildConfiguration,
applicationArchivePath: job.applicationArchivePath, artifactPath: job.artifactPath,
buildArtifactPaths: job.buildArtifactPaths,
username: job.username, username: job.username,
developmentClient: job.developmentClient, developmentClient: job.developmentClient,
simulator: job.simulator, simulator: job.simulator,
experimental: job.experimental, experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
}; };
} }
export function transformIosSecrets(secrets: { function transformIosSecrets(secrets: {
buildCredentials?: Ios.BuildCredentials; buildCredentials?: Ios.BuildCredentials;
env?: Env;
}): IosJobSecretsInput { }): IosJobSecretsInput {
const buildCredentials: IosJobSecretsInput['buildCredentials'] = []; const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
@@ -51,5 +42,6 @@ export function transformIosSecrets(secrets: {
return { return {
buildCredentials, buildCredentials,
environmentSecrets: secrets.env,
}; };
} }
+21 -65
View File
@@ -1,21 +1,10 @@
import { import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
ArchiveSource,
BuildMode,
BuildTrigger,
Ios,
Job,
Platform,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import path from 'path'; import path from 'path';
import slash from 'slash'; import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types'; import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { ensureLoggedInAsync } from '../../user/actions';
import { getVcsClient } from '../../vcs'; import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -28,17 +17,17 @@ interface JobData {
const cacheDefaults = { const cacheDefaults = {
disabled: false, disabled: false,
customPaths: [], customPaths: [],
cacheDefaultPaths: true,
}; };
export async function prepareJobAsync( export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>, ctx: BuildContext<Platform.IOS>,
jobData: JobData jobData: JobData
): Promise<Job> { ): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory = const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.'; slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {}; const username = getUsername(ctx.exp, await ensureLoggedInAsync());
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) { if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials); const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) { for (const targetName of targetNames) {
@@ -46,77 +35,44 @@ export async function prepareJobAsync(
} }
} }
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Ios.Job = { const job: Ios.Job = {
type: ctx.workflow, type: ctx.workflow,
platform: Platform.IOS, platform: Platform.IOS,
projectArchive: jobData.projectArchive, projectArchive: jobData.projectArchive,
projectRootDirectory, projectRootDirectory,
builderEnvironment: { builderEnvironment: {
image: buildProfile.image, image: ctx.buildProfile.image,
node: buildProfile.node, node: ctx.buildProfile.node,
yarn: buildProfile.yarn, yarn: ctx.buildProfile.yarn,
bundler: buildProfile.bundler, bundler: ctx.buildProfile.bundler,
cocoapods: buildProfile.cocoapods, cocoapods: ctx.buildProfile.cocoapods,
fastlane: buildProfile.fastlane, fastlane: ctx.buildProfile.fastlane,
expoCli: buildProfile.expoCli, expoCli: ctx.buildProfile.expoCli,
env: buildProfile.env, env: ctx.buildProfile.env,
}, },
cache: { cache: {
...cacheDefaults, ...cacheDefaults,
...buildProfile.cache, ...ctx.buildProfile.cache,
clear: ctx.clearCache, clear: ctx.clearCache,
}, },
secrets: { secrets: {
buildCredentials, buildCredentials,
}, },
releaseChannel: buildProfile.releaseChannel, releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: buildProfile.channel }, updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient, developmentClient: ctx.buildProfile.developmentClient,
simulator: buildProfile.simulator, simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme, scheme: jobData.buildScheme,
buildConfiguration: buildProfile.buildConfiguration, buildConfiguration: ctx.buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath, artifactPath: ctx.buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
username, username,
...(ctx.ios.buildNumberOverride && {
version: {
buildNumber: ctx.ios.buildNumberOverride,
},
}),
experimental: { experimental: {
prebuildCommand: buildProfile.prebuildCommand, prebuildCommand: ctx.buildProfile.prebuildCommand,
}, },
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
}; };
return sanitizeJob(job); 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 { function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
return { return {
provisioningProfileBase64: targetCredentials.provisioningProfile, provisioningProfileBase64: targetCredentials.provisioningProfile,
@@ -1,38 +1,33 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { IosVersionAutoIncrement } from '@expo/eas-json'; import { BuildProfile, IosVersionAutoIncrement } from '@expo/eas-json';
import type { XCBuildConfiguration } from 'xcode';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils'; import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule'; import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version'; import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync( export async function syncProjectConfigurationAsync({
graphqlClient: ExpoGraphqlClient, projectDir,
{ exp,
projectDir, buildProfile,
exp, buildSettings,
targets, }: {
localAutoIncrement, projectDir: string;
projectId, exp: ExpoConfig;
}: { buildProfile: BuildProfile<Platform.IOS>;
projectDir: string; buildSettings: XCBuildConfiguration['buildSettings'];
exp: ExpoConfig; }): Promise<void> {
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const { autoIncrement } = buildProfile;
const versionBumpStrategy = resolveVersionBumpStrategy(autoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId); await syncUpdatesConfigurationAsync(projectDir, exp);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, buildSettings });
} else { } else {
await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy }); await bumpVersionInAppJsonAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} }
+43 -185
View File
@@ -1,26 +1,16 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig, getConfigFilePaths } from '@expo/config';
import { IOSConfig, Updates } from '@expo/config-plugins'; import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import path from 'path'; import path from 'path';
import type { XCBuildConfiguration } from 'xcode'; 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 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 { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts'; import { promptAsync } from '../../prompts';
import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist'; import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { updateAppJsonConfigAsync } from '../utils/appJson'; import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version'; import { bumpAppVersionAsync } from '../utils/version';
export enum BumpStrategy { export enum BumpStrategy {
APP_VERSION, APP_VERSION,
@@ -32,25 +22,21 @@ export async function bumpVersionAsync({
bumpStrategy, bumpStrategy,
projectDir, projectDir,
exp, exp,
targets, buildSettings,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
bumpStrategy: BumpStrategy; bumpStrategy: BumpStrategy;
targets: Target[]; buildSettings: XCBuildConfiguration['buildSettings'];
}): Promise<void> { }): Promise<void> {
if (bumpStrategy === BumpStrategy.NOOP) { if (bumpStrategy === BumpStrategy.NOOP) {
return; return;
} }
ensureStaticConfigExists(projectDir); ensureStaticConfigExists(projectDir);
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp }); await bumpVersionInAppJsonAsync({ bumpStrategy, projectDir, exp });
Log.log('Updated versions in app.json'); Log.log('Updated versions in app.json');
await updateNativeVersionsAsync({ await writeVersionsToInfoPlistAsync({ projectDir, exp, infoPlist, buildSettings });
projectDir,
version: exp.version,
buildNumber: exp.ios?.buildNumber,
targets,
});
Log.log('Synchronized versions with Info.plist'); Log.log('Synchronized versions with Info.plist');
} }
@@ -73,8 +59,10 @@ export async function bumpVersionInAppJsonAsync({
await bumpAppVersionAsync({ appVersion, projectDir, exp }); await bumpAppVersionAsync({ appVersion, projectDir, exp });
} else { } else {
const buildNumber = IOSConfig.Version.getBuildNumber(exp); const buildNumber = IOSConfig.Version.getBuildNumber(exp);
if (isValidBuildNumber(buildNumber)) { if (buildNumber.match(/^\d+(\.\d+)*$/)) {
const bumpedBuildNumber = getNextBuildNumber(buildNumber); const comps = buildNumber.split('.');
comps[comps.length - 1] = String(Number(comps[comps.length - 1]) + 1);
const bumpedBuildNumber = comps.join('.');
Log.log( Log.log(
`Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold( `Bumping ${chalk.bold('expo.ios.buildNumber')} from ${chalk.bold(
buildNumber buildNumber
@@ -133,21 +121,12 @@ export async function readBuildNumberAsync(
export async function maybeResolveVersionsAsync( export async function maybeResolveVersionsAsync(
projectDir: string, projectDir: string,
exp: ExpoConfig, exp: ExpoConfig,
targets: Target[] buildSettings: XCBuildConfiguration['buildSettings']
): Promise<{ appVersion?: string; appBuildVersion?: string }> { ): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const applicationTarget = findApplicationTarget(targets);
try { try {
return { return {
appBuildVersion: await readBuildNumberAsync( appBuildVersion: await readBuildNumberAsync(projectDir, exp, buildSettings),
projectDir, appVersion: await readShortVersionAsync(projectDir, exp, buildSettings),
exp,
applicationTarget.buildSettings ?? {}
),
appVersion: await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {}
),
}; };
} catch (err: any) { } catch (err: any) {
Log.warn('Failed to read app versions.'); Log.warn('Failed to read app versions.');
@@ -158,6 +137,23 @@ export async function maybeResolveVersionsAsync(
} }
} }
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( export function getInfoPlistPath(
projectDir: string, projectDir: string,
buildSettings: XCBuildConfiguration['buildSettings'] buildSettings: XCBuildConfiguration['buildSettings']
@@ -185,55 +181,24 @@ async function readInfoPlistAsync(
return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist; return ((await readPlistAsync(infoPlistPath)) ?? {}) as IOSConfig.InfoPlist;
} }
export async function updateNativeVersionsAsync({ async function writeInfoPlistAsync({
projectDir, projectDir,
version, infoPlist,
buildNumber, buildSettings,
targets,
}: { }: {
projectDir: string; projectDir: string;
version?: string; infoPlist: IOSConfig.InfoPlist;
buildNumber?: string; buildSettings: XCBuildConfiguration['buildSettings'];
targets: Target[];
}): Promise<void> { }): Promise<void> {
const project = IOSConfig.XcodeUtils.getPbxproj(projectDir); const infoPlistPath = getInfoPlistPath(projectDir, buildSettings);
const iosDir = path.join(projectDir, 'ios'); 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 trimQuotes(s: string): string { function ensureStaticConfigExists(projectDir: string): void {
return s?.startsWith('"') && s.endsWith('"') ? s.slice(1, -1) : s; const paths = getConfigFilePaths(projectDir);
if (!paths.staticConfigPath) {
throw new Error('autoIncrement option is not supported when using app.config.js');
}
} }
export function evaluateTemplateString( export function evaluateTemplateString(
@@ -251,110 +216,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;
}
}
+9 -34
View File
@@ -1,4 +1,4 @@
import { Job, Metadata } from '@expo/eas-build-job'; import { Job } from '@expo/eas-build-job';
import spawnAsync from '@expo/spawn-async'; import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import semver from 'semver'; import semver from 'semver';
@@ -6,28 +6,10 @@ import semver from 'semver';
import { ora } from '../ora'; import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin'; const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '0.0.128'; const PLUGIN_PACKAGE_VERSION = '0.0.78';
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 { export interface LocalBuildOptions {
localBuildMode?: LocalBuildMode; enable: boolean;
skipCleanup?: boolean; skipCleanup?: boolean;
skipNativeBuild?: boolean; skipNativeBuild?: boolean;
artifactsDir?: string; artifactsDir?: string;
@@ -36,12 +18,8 @@ export interface LocalBuildOptions {
verbose?: boolean; verbose?: boolean;
} }
export async function runLocalBuildAsync( export async function runLocalBuildAsync(job: Job, options: LocalBuildOptions): Promise<void> {
job: Job, const { command, args } = await getCommandAndArgsAsync(job);
metadata: Metadata,
options: LocalBuildOptions
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner; let spinner;
if (!options.verbose) { if (!options.verbose) {
spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project'); spinner = ora().start(options.skipNativeBuild ? 'Preparing project' : 'Building project');
@@ -75,18 +53,15 @@ export async function runLocalBuildAsync(
} }
} }
async function getCommandAndArgsAsync( async function getCommandAndArgsAsync(job: Job): Promise<{ command: string; args: string[] }> {
job: Job, const jobBase64 = Buffer.from(JSON.stringify({ job })).toString('base64');
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) { if (process.env.EAS_LOCAL_BUILD_PLUGIN_PATH) {
return { return {
command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH, command: process.env.EAS_LOCAL_BUILD_PLUGIN_PATH,
args: [jobAndMetadataBase64], args: [jobBase64],
}; };
} else { } else {
const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobAndMetadataBase64]; const args = [`${PLUGIN_PACKAGE_NAME}@${PLUGIN_PACKAGE_VERSION}`, jobBase64];
if (await isAtLeastNpm7Async()) { if (await isAtLeastNpm7Async()) {
// npx shipped with npm >= 7.0.0 requires the "-y" flag to run commands without // 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 // prompting the user to install a package that is used for the first time
+15 -48
View File
@@ -1,5 +1,5 @@
import { Updates } from '@expo/config-plugins'; import { Updates } from '@expo/config-plugins';
import { BuildMode, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job'; import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json'; import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra'; import fs from 'fs-extra';
import resolveFrom from 'resolve-from'; import resolveFrom from 'resolve-from';
@@ -14,6 +14,7 @@ import {
readChannelSafelyAsync as readIosChannelSafelyAsync, readChannelSafelyAsync as readIosChannelSafelyAsync,
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync, readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule'; } from '../update/ios/UpdatesModule';
import { ensureLoggedInAsync } from '../user/actions';
import { easCliVersion } from '../utils/easCli'; import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version'; import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
@@ -23,41 +24,33 @@ import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios
export async function collectMetadataAsync<T extends Platform>( export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T> ctx: BuildContext<T>
): Promise<Metadata> { ): Promise<Metadata> {
const vcsClient = getVcsClient();
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx); const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution = const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator ('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator' ? 'simulator'
: ctx.buildProfile.distribution) ?? 'store'; : ctx.buildProfile.distribution) ?? 'store';
const metadata: Metadata = { const metadata = {
trackingContext: ctx.analyticsEventProperties, trackingContext: ctx.trackingCtx,
...(await maybeResolveVersionsAsync(ctx)), ...(await maybeResolveVersionsAsync(ctx)),
cliVersion: easCliVersion, cliVersion: easCliVersion,
workflow: ctx.workflow, workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource, credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion, sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined, runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir), reactNativeVersion: await getReactNativeVersionAsync(ctx),
...channelOrReleaseChannel, ...channelOrReleaseChannel,
distribution, distribution,
appName: ctx.exp.name, appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx), appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName, buildProfile: ctx.buildProfileName,
gitCommitHash: await vcsClient.getCommitHashAsync(), gitCommitHash: await getVcsClient().getCommitHashAsync(),
gitCommitMessage: truncateGitCommitMessage( isGitWorkingTreeDirty: await getVcsClient().hasUncommittedChangesAsync(),
(await vcsClient.getLastCommitMessageAsync()) ?? undefined username: getUsername(ctx.exp, await ensureLoggedInAsync()),
),
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && { ...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning( iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
ctx as BuildContext<Platform.IOS> ctx as BuildContext<Platform.IOS>
), ),
}), }),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
}; };
return sanitizeMetadata(metadata); return sanitizeMetadata(metadata);
} }
@@ -67,32 +60,14 @@ async function maybeResolveVersionsAsync<T extends Platform>(
): Promise<{ appBuildVersion?: string; appVersion?: string }> { ): Promise<{ appBuildVersion?: string; appVersion?: string }> {
if (ctx.platform === Platform.IOS) { if (ctx.platform === Platform.IOS) {
const iosContext = ctx as BuildContext<Platform.IOS>; const iosContext = ctx as BuildContext<Platform.IOS>;
const resolvedVersion = await maybeResolveIosVersionsAsync( return await maybeResolveIosVersionsAsync(
ctx.projectDir, ctx.projectDir,
ctx.exp, ctx.exp,
iosContext.ios.targets iosContext.ios.applicationTargetBuildSettings
); );
if (iosContext.ios.buildNumberOverride) {
return {
...resolvedVersion,
appBuildVersion: iosContext.ios.buildNumberOverride,
};
}
return resolvedVersion;
} else if (ctx.platform === Platform.ANDROID) { } else if (ctx.platform === Platform.ANDROID) {
const androidCtx = ctx as BuildContext<Platform.ANDROID>; const androidCtx = ctx as BuildContext<Platform.ANDROID>;
const resolvedVersion = await maybeResolveAndroidVersionsAsync( return await maybeResolveAndroidVersionsAsync(ctx.projectDir, ctx.exp, androidCtx.buildProfile);
ctx.projectDir,
ctx.exp,
androidCtx.buildProfile
);
if (androidCtx.android.versionCodeOverride) {
return {
...resolvedVersion,
appBuildVersion: androidCtx.android.versionCodeOverride,
};
}
return resolvedVersion;
} else { } else {
throw new Error(`Unsupported platform ${ctx.platform}`); throw new Error(`Unsupported platform ${ctx.platform}`);
} }
@@ -156,9 +131,11 @@ async function getNativeChannelAsync<T extends Platform>(
return undefined; return undefined;
} }
export async function getReactNativeVersionAsync(projectDir: string): Promise<string | undefined> { async function getReactNativeVersionAsync(
ctx: BuildContext<Platform>
): Promise<string | undefined> {
try { try {
const reactNativePackageJsonPath = resolveFrom(projectDir, 'react-native/package.json'); const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
return (await fs.readJson(reactNativePackageJsonPath)).version; return (await fs.readJson(reactNativePackageJsonPath)).version;
} catch (err) { } catch (err) {
Log.debug('Failed to resolve react-native version:'); Log.debug('Failed to resolve react-native version:');
@@ -172,13 +149,3 @@ function resolveIosEnterpriseProvisioning(
): IosEnterpriseProvisioning | undefined { ): IosEnterpriseProvisioning | undefined {
return ctx.buildProfile.enterpriseProvisioning; 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;
}
-193
View File
@@ -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}`);
}
}
+13 -159
View File
@@ -1,28 +1,9 @@
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { import { BuildProfile, SubmitProfile } from '@expo/eas-json';
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
ResourceClass,
SubmitProfile,
} from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import { Analytics } from '../analytics/AnalyticsManager'; import { AppPlatform, BuildFragment, BuildStatus, SubmissionFragment } from '../graphql/generated';
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 { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log from '../log'; import Log from '../log';
import { import {
@@ -31,33 +12,19 @@ import {
appPlatformEmojis, appPlatformEmojis,
toPlatforms, toPlatforms,
} from '../platform'; } from '../platform';
import { validateCustomBuildConfigAsync } from '../project/customBuildConfig';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk'; import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig'; 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 { createSubmissionContextAsync } from '../submit/context';
import { import {
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
submitAsync, submitAsync,
waitToCompleteAsync as waitForSubmissionsToCompleteAsync, waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit'; } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls'; 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 { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles'; import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build'; import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build'; import { BuildRequestSender, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure'; import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context'; import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext'; import { createBuildContextAsync } from './createContext';
@@ -80,18 +47,9 @@ export interface BuildFlags {
autoSubmit: boolean; autoSubmit: boolean;
submitProfile?: string; submitProfile?: string;
localBuildOptions: LocalBuildOptions; localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
} }
export async function runBuildAndSubmitAsync( export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicProjectConfigAsync: DynamicConfigContextFn
): Promise<void> {
await getVcsClient().ensureRepoExistsAsync(); await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive); await ensureRepoIsCleanAsync(flags.nonInteractive);
@@ -99,14 +57,11 @@ export async function runBuildAndSubmitAsync(
projectDir, projectDir,
nonInteractive: flags.nonInteractive, nonInteractive: flags.nonInteractive,
}); });
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
const platforms = toPlatforms(flags.requestedPlatform); const platforms = toPlatforms(flags.requestedPlatform);
const buildProfiles = await getProfilesAsync({ const buildProfiles = await getProfilesAsync({
type: 'build', type: 'build',
easJsonAccessor, projectDir,
platforms, platforms,
profileName: flags.profile ?? undefined, profileName: flags.profile ?? undefined,
}); });
@@ -117,13 +72,8 @@ export async function runBuildAndSubmitAsync(
buildProfiles, buildProfiles,
}); });
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
}
const startedBuilds: { const startedBuilds: {
build: BuildWithSubmissionsFragment | BuildFragment; build: BuildFragment;
buildProfile: ProfileData<'build'>; buildProfile: ProfileData<'build'>;
}[] = []; }[] = [];
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {}; const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
@@ -134,11 +84,6 @@ export async function runBuildAndSubmitAsync(
flags, flags,
moreBuilds: platforms.length > 1, moreBuilds: platforms.length > 1,
buildProfile, buildProfile,
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}); });
if (maybeBuild) { if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile }); startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -146,7 +91,7 @@ export async function runBuildAndSubmitAsync(
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx; buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
} }
if (flags.localBuildOptions.localBuildMode) { if (flags.localBuildOptions.enable) {
return; return;
} }
@@ -157,7 +102,7 @@ export async function runBuildAndSubmitAsync(
const submissions: SubmissionFragment[] = []; const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) { if (flags.autoSubmit) {
const submitProfiles = await getProfilesAsync({ const submitProfiles = await getProfilesAsync({
easJsonAccessor, projectDir,
platforms, platforms,
profileName: flags.submitProfile, profileName: flags.submitProfile,
type: 'submit', type: 'submit',
@@ -177,10 +122,6 @@ export async function runBuildAndSubmitAsync(
submitProfile, submitProfile,
nonInteractive: flags.nonInteractive, nonInteractive: flags.nonInteractive,
}); });
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
graphqlClient,
startedBuild.build.id
);
submissions.push(submission); submissions.push(submission);
} }
@@ -197,42 +138,20 @@ export async function runBuildAndSubmitAsync(
} }
const { accountName } = Object.values(buildCtxByPlatform)[0]; const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync(graphqlClient, { const builds = await waitForBuildEndAsync({
buildIds: startedBuilds.map(({ build }) => build.id), buildIds: startedBuilds.map(({ build }) => build.id),
accountName, accountName,
projectDir,
nonInteractive: flags.nonInteractive,
}); });
if (!flags.json) { printBuildResults(builds, flags.json);
printBuildResults(builds);
}
const haveAllBuildsFailedOrCanceled = builds.every( const haveAllBuildsFailedOrCanceled = builds.every(
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status) build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
); );
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) { if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
printJsonOnlyOutput(builds);
}
exitWithNonZeroCodeIfSomeBuildsFailed(builds); exitWithNonZeroCodeIfSomeBuildsFailed(builds);
} else { } else {
const completedSubmissions = await waitForSubmissionsToCompleteAsync( // the following function also exits with non zero code if any of the submissions failed
graphqlClient, await waitForSubmissionsToCompleteAsync(submissions);
submissions
);
if (flags.json) {
printJsonOnlyOutput(
await Promise.all(
builds
.filter((i): i is BuildWithSubmissionsFragment => !!i)
.map(build => BuildQuery.withSubmissionsByIdAsync(graphqlClient, build.id))
)
);
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
} }
} }
@@ -241,38 +160,20 @@ async function prepareAndStartBuildAsync({
flags, flags,
moreBuilds, moreBuilds,
buildProfile, buildProfile,
easJsonCliConfig,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}: { }: {
projectDir: string; projectDir: string;
flags: BuildFlags; flags: BuildFlags;
moreBuilds: boolean; moreBuilds: boolean;
buildProfile: ProfileData<'build'>; buildProfile: ProfileData<'build'>;
easJsonCliConfig: EasJson['cli'];
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
getDynamicProjectConfigAsync: DynamicConfigContextFn;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> { }): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({ const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName, buildProfileName: buildProfile.profileName,
resourceClassFlag: flags.resourceClass,
clearCache: flags.clearCache, clearCache: flags.clearCache,
buildProfile: buildProfile.profile, buildProfile: buildProfile.profile,
nonInteractive: flags.nonInteractive, nonInteractive: flags.nonInteractive,
noWait: !flags.wait,
platform: buildProfile.platform, platform: buildProfile.platform,
projectDir, projectDir,
localBuildOptions: flags.localBuildOptions, localBuildOptions: flags.localBuildOptions,
easJsonCliConfig,
message: flags.message,
actor,
graphqlClient,
analytics,
getDynamicProjectConfigAsync,
}); });
if (moreBuilds) { if (moreBuilds) {
@@ -285,17 +186,6 @@ async function prepareAndStartBuildAsync({
); );
} }
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 (buildCtx.workflow === Workflow.MANAGED) {
if (!sdkVersionChecked) { if (!sdkVersionChecked) {
await checkExpoSdkIsSupportedAsync(buildCtx); await checkExpoSdkIsSupportedAsync(buildCtx);
@@ -345,17 +235,13 @@ async function prepareAndStartSubmissionAsync({
const submissionCtx = await createSubmissionContextAsync({ const submissionCtx = await createSubmissionContextAsync({
platform, platform,
projectDir, projectDir,
projectId: build.project.id,
profile: submitProfile, profile: submitProfile,
archiveFlags: { id: build.id }, archiveFlags: { id: build.id },
nonInteractive, nonInteractive,
env: buildProfile.env, env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx, credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier, applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
graphqlClient: buildCtx.graphqlClient,
analytics: buildCtx.analytics,
projectId: buildCtx.projectId,
exp: buildCtx.exp,
}); });
if (moreBuilds) { if (moreBuilds) {
@@ -378,35 +264,3 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
process.exit(1); 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);
}
}
}
}
}
@@ -38,10 +38,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
} }
}, },
}, },
{
label: 'Message',
value: build.message,
},
{ {
label: 'Distribution', label: 'Distribution',
value: build.distribution?.toLowerCase(), value: build.distribution?.toLowerCase(),
@@ -1,4 +1,5 @@
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk';
import indentString from 'indent-string'; import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal'; import qrcodeTerminal from 'qrcode-terminal';
@@ -11,64 +12,44 @@ import {
EasBuildDeprecationInfo, EasBuildDeprecationInfo,
EasBuildDeprecationInfoType, EasBuildDeprecationInfoType,
} from '../../graphql/generated'; } from '../../graphql/generated';
import Log, { learnMore, link } from '../../log'; import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform'; import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { printJsonOnlyOutput } from '../../utils/json';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url'; 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 { export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) { if (builds.length === 1) {
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`); Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
} else { } else {
builds.forEach(build => { builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build); const logsUrl = getBuildLogsUrl(build);
Log.log( Log.log(
`${appPlatformEmojis[build.platform]} ${ `${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform] appPlatformDisplayNames[build.platform]
} build details: ${link(logsUrl)}` } build details: ${chalk.underline(logsUrl)}`
); );
}); });
} }
} }
export function printBuildResults(builds: (BuildFragment | null)[]): void { export function printBuildResults(builds: (BuildFragment | null)[], json: boolean): void {
if (builds.length === 1) { if (json) {
const [build] = builds; printJsonOnlyOutput(builds);
assert(build, 'Build should be defined');
printBuildResult(build);
} else { } 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));
}
} }
} }
function printBuildResult(build: BuildFragment): void { function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) { if (build.status === BuildStatus.Errored) {
Log.addNewLineIfNone();
const userError = build.error; const userError = build.error;
Log.error( Log.error(
`${appPlatformEmojis[build.platform]} ${ `${appPlatformEmojis[build.platform]} ${
@@ -76,12 +57,11 @@ function printBuildResult(build: BuildFragment): void {
} build failed${userError ? ':' : ''}` } build failed${userError ? ':' : ''}`
); );
if (userError) { if (userError) {
printUserError(userError, build); printUserError(userError);
} }
return; return;
} }
if (build.status === BuildStatus.Canceled) { if (build.status === BuildStatus.Canceled) {
Log.addNewLineIfNone();
Log.error( Log.error(
`${appPlatformEmojis[build.platform]} ${ `${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform] appPlatformDisplayNames[build.platform]
@@ -91,7 +71,6 @@ function printBuildResult(build: BuildFragment): void {
} }
if (build.distribution === DistributionType.Internal) { if (build.distribution === DistributionType.Internal) {
Log.addNewLineIfNone();
const logsUrl = getBuildLogsUrl(build); const logsUrl = getBuildLogsUrl(build);
// It's tricky to install the .apk file directly on Android so let's fallback // It's tricky to install the .apk file directly on Android so let's fallback
// to the build details page and let people press the button to download there // to the build details page and let people press the button to download there
@@ -105,18 +84,13 @@ function printBuildResult(build: BuildFragment): void {
appPlatformDisplayNames[build.platform] appPlatformDisplayNames[build.platform]
} devices (or scan the QR code) to install the app:` } devices (or scan the QR code) to install the app:`
); );
Log.log(`${link(logsUrl)}`); Log.log(`${chalk.underline(logsUrl)}`);
} else { } else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below. // 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 // we should account for this case better if it is possible
const url = build.artifacts?.buildUrl; const url = build.artifacts?.buildUrl ?? '';
if (url) { Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.addNewLineIfNone(); Log.log(`${chalk.underline(url)}`);
Log.log(
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
);
Log.log(link(url));
}
} }
} }
@@ -125,34 +99,21 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
return; return;
} }
if (deprecationInfo.type === EasBuildDeprecationInfoType.Internal) { 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("Changes won't affect your project config.");
Log.warn(deprecationInfo.message); Log.warn(deprecationInfo.message);
} else if (deprecationInfo.type === EasBuildDeprecationInfoType.UserFacing) { } 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.');
Log.warn(deprecationInfo.message); Log.warn(deprecationInfo.message);
} else { } 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); Log.warn(deprecationInfo);
} }
} }
export function printUserError(error: BuildError, build: BuildFragment): void { export function printUserError(error: BuildError): void {
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build); Log.error(error.message);
if (maybeErrorMessageOverride) { if (error.docsUrl) {
Log.error(maybeErrorMessageOverride); Log.error(learnMore(error.docsUrl, { dim: false }));
} else {
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);
}
+64 -10
View File
@@ -1,3 +1,4 @@
import admzip from 'adm-zip';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
@@ -28,7 +29,7 @@ export async function maybeBailOnRepoStatusAsync(): Promise<void> {
}); });
if (!answer) { if (!answer) {
throw new Error('Commit all changes. Aborting...'); throw new Error('Please commit all changes. Aborting...');
} }
} }
@@ -39,12 +40,12 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
Log.addNewLineIfNone(); Log.addNewLineIfNone();
Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`); Log.warn(`${chalk.bold('Warning!')} Your repository working tree is dirty.`);
Log.log( Log.log(
`This operation needs to be run on a clean working tree. ${chalk.bold( `This operation needs to be run on a clean working tree, please ${chalk.bold(
'Commit all your changes before proceeding' 'commit all your changes before proceeding'
)}.` )}.`
); );
if (nonInteractive) { if (nonInteractive) {
throw new Error('Commit all changes. Aborting...'); throw new Error('Please commit all changes. Aborting...');
} }
const answer = await confirmAsync({ const answer = await confirmAsync({
message: `Commit changes to git?`, message: `Commit changes to git?`,
@@ -52,7 +53,7 @@ export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<vo
if (answer) { if (answer) {
await commitPromptAsync({ commitAllFiles: true }); await commitPromptAsync({ commitAllFiles: true });
} else { } else {
throw new Error('Commit all changes. Aborting...'); throw new Error('Please commit all changes. Aborting...');
} }
} }
@@ -70,11 +71,7 @@ export async function commitPromptAsync({
initial: initialCommitMessage, initial: initialCommitMessage,
validate: (input: string) => input !== '', validate: (input: string) => input !== '',
}); });
await getVcsClient().commitAsync({ await getVcsClient().commitAsync({ commitAllFiles, commitMessage: message });
commitAllFiles,
commitMessage: message,
nonInteractive: false,
});
} }
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> { export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
@@ -126,6 +123,63 @@ export async function makeProjectTarballAsync(): Promise<{ path: string; size: n
return { size, path: tarPath }; return { size, path: tarPath };
} }
export async function makeZipAsync(pathToZip: string): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
const shallowClonePath = path.join(getTmpDirectory(), `${uuidv4()}-shallow-clone`);
try {
process.chdir(pathToZip);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
process.chdir('..');
const zipPath = path.join(getTmpDirectory(), `${uuidv4()}.zip`);
// If the compression takes longer then a second, show the spinner.
// This can happen when the user has a lot of resources or doesn't ignore their CocoaPods.
// A basic project on a Mac can compress in roughly ~40ms.
// A fairly complex project without CocoaPods ignored can take up to 30s.
const timer = setTimeout(
() => {
spinner.start();
},
Log.isDebug ? 1 : 1000
);
// TODO: Possibly warn after more time about unoptimized assets.
const compressTimerLabel = 'makeProjectZipAsync';
startTimer(compressTimerLabel);
const zip = new admzip();
zip.addLocalFolder(shallowClonePath);
await zipAsync(zip, zipPath);
clearTimeout(timer);
const { size } = await fs.stat(zipPath);
const duration = endTimer(compressTimerLabel);
if (spinner.isSpinning) {
const prettyTime = formatMilliseconds(duration);
spinner.succeed(
`Compressed project files ${chalk.dim(`${prettyTime} (${formatBytes(size)})`)}`
);
}
return { size, path: zipPath };
} catch (err) {
if (spinner.isSpinning) {
spinner.fail();
}
throw err;
} finally {
await fs.remove(shallowClonePath);
}
}
async function zipAsync(zip: admzip, targetFileName?: string): Promise<void> {
return new Promise((resolve, reject) => {
zip.writeZip(targetFileName, (error: Error | null) => (error ? reject(error) : resolve()));
});
}
enum ShouldCommitChanges { enum ShouldCommitChanges {
Yes, Yes,
ShowDiffFirst, ShowDiffFirst,
@@ -1,123 +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';
type AndroidResourceClass = Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
| ResourceClass.M_MEDIUM
| ResourceClass.M_LARGE
>;
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
};
const androidResourceClassToBuildResourceClassMapping: Record<
AndroidResourceClass,
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 AndroidResourceClass];
}
async function resolveIosResourceClassAsync(
exp: ExpoConfig,
projectDir: string,
selectedResourceClass?: ResourceClass
): Promise<BuildResourceClass> {
const resourceClass =
selectedResourceClass ?? (await resolveIosDefaultRequestedResourceClassAsync(exp, projectDir));
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'm-medium'
)} instead.`
);
}
if (resourceClass === ResourceClass.M1_LARGE) {
Log.warn(
`Resource class ${chalk.bold('m1-large')} is deprecated. Use ${chalk.bold(
'm-large'
)} instead.`
);
}
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.M_MEDIUM;
} else {
return ResourceClass.DEFAULT;
}
}
+2 -17
View File
@@ -10,14 +10,12 @@ export function getProjectDashboardUrl(accountName: string, projectName: string)
).toString(); ).toString();
} }
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string { export function getBuildLogsUrl(build: BuildFragment): string {
const { project } = build; const { project } = build;
const url = const url =
project.__typename !== 'App' project.__typename !== 'App'
? `/builds/${build.id}` ? `/builds/${build.id}`
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}${ : `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
hash ? `#${hash}` : ''
}`;
return new URL(url, getExpoWebsiteBaseUrl()).toString(); return new URL(url, getExpoWebsiteBaseUrl()).toString();
} }
@@ -37,16 +35,3 @@ export function getInternalDistributionInstallUrl(build: BuildFragment): string
return build.artifacts.buildUrl; 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();
}
-137
View File
@@ -1,11 +1,8 @@
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors } from '@oclif/core';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import semver from 'semver';
import Log, { learnMore } from '../log'; import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
import { getVcsClient } from '../vcs'; import { getVcsClient } from '../vcs';
import { CommonContext } from './context'; import { CommonContext } from './context';
@@ -53,137 +50,3 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
function isInsideDirectory(file: string, directory: string): boolean { function isInsideDirectory(file: string, directory: string): boolean {
return file.startsWith(directory); 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`);
}
}
-5
View File
@@ -1,5 +0,0 @@
export class ChannelNotFoundError extends Error {
constructor(message?: string) {
super(message ?? 'Channel not found.');
}
}
-264
View File
@@ -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;
}
}
}
-117
View File
@@ -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`)
);
}
}
+42 -173
View File
@@ -1,200 +1,69 @@
import { EasJsonReader } from '@expo/eas-json';
import { Command } from '@oclif/core'; import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core'; import semver from 'semver';
import nullthrows from 'nullthrows';
import { import {
AnalyticsWithOrchestration, AnalyticsEvent,
CommandEvent, flushAsync as flushAnalyticsAsync,
createAnalyticsAsync, initAsync as initAnalyticsAsync,
} from '../analytics/AnalyticsManager'; logEvent,
import Log from '../log'; } from '../analytics/rudderstackClient';
import SessionManager from '../user/SessionManager'; import { findProjectRootAsync } from '../project/projectUtils';
import AnalyticsContextField from './context/AnalyticsContextField'; import { getUserAsync } from '../user/User';
import ContextField from './context/ContextField'; import { ensureLoggedInAsync } from '../user/actions';
import { DynamicProjectConfigContextField } from './context/DynamicProjectConfigContextField'; import { easCliVersion } from '../utils/easCli';
import LoggedInContextField from './context/LoggedInContextField'; import { setVcsClient } from '../vcs';
import MaybeLoggedInContextField from './context/MaybeLoggedInContextField'; import GitClient from '../vcs/clients/git';
import { OptionalProjectConfigContextField } from './context/OptionalProjectConfigContextField';
import ProjectConfigContextField from './context/ProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import { EasCommandError } from './errors';
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];
};
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
export default abstract class EasCommand extends Command { 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. * When user data is unavailable locally, determines if the command will
* These declarative definitions each output a context property that is the result of the prerequisite being * force the user to log in
* 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.
*/ */
static contextDefinition: ContextInput = {}; protected requiresAuthentication = true;
protected mustBeRunInsideProject = true;
/**
* The user session manager. Responsible for coordinating all user session related state.
* If needed in a subclass, use the SessionManager ContextOption.
*/
private sessionManagerInternal?: SessionManager;
/**
* The analytics manager. Used for logging analytics.
* It is set up here to ensure a consistent setup.
*/
private analyticsInternal?: AnalyticsWithOrchestration;
/**
* 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);
}
private get sessionManager(): SessionManager {
return nullthrows(this.sessionManagerInternal);
}
private get analytics(): AnalyticsWithOrchestration {
return nullthrows(this.analyticsInternal);
}
protected abstract runAsync(): Promise<any>; protected abstract runAsync(): Promise<any>;
// eslint-disable-next-line async-protect/async-suffix // eslint-disable-next-line async-protect/async-suffix
async run(): Promise<any> { async run(): Promise<any> {
this.analyticsInternal = await createAnalyticsAsync(); await initAnalyticsAsync();
this.sessionManagerInternal = new SessionManager(this.analytics); if (this.mustBeRunInsideProject) {
await this.applyCliConfigAsync();
}
// this is needed for logEvent call below as it identifies the user in the analytics system if (this.requiresAuthentication) {
// if possible const { flags } = await this.parse();
await this.sessionManager.getUserAsync(); const nonInteractive = (flags as any)['non-interactive'] ?? false;
await ensureLoggedInAsync({ nonInteractive });
this.analytics.logEvent(CommandEvent.ACTION, { } else {
await getUserAsync();
}
logEvent(AnalyticsEvent.ACTION, {
// id is assigned by oclif in constructor based on the filepath: // id is assigned by oclif in constructor based on the filepath:
// commands/submit === submit, commands/build/list === build:list // commands/submit === submit, commands/build/list === build:list
action: `eas ${this.id}`, action: `eas ${this.id}`,
}); });
return this.runAsync(); return this.runAsync();
} }
// eslint-disable-next-line async-protect/async-suffix // eslint-disable-next-line async-protect/async-suffix
override async finally(err: Error): Promise<any> { async finally(err: Error): Promise<any> {
await this.analytics.flushAsync(); await flushAnalyticsAsync();
return super.finally(err); return super.finally(err);
} }
protected override catch(err: Error): Promise<any> { private async applyCliConfigAsync(): Promise<void> {
let baseMessage = `${this.id} command failed.`; const projectDir = await findProjectRootAsync();
if (err instanceof EasCommandError) { const easJsonReader = new EasJsonReader(projectDir);
Log.error(err.message); const config = await easJsonReader.getCliConfigAsync();
} else if (err instanceof CombinedError && err?.graphQLErrors) { if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
const cleanMessage = err.message.replace('[GraphQL] ', ''); throw new Error(
Log.error(cleanMessage); `You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE; );
} else { }
Log.error(err.message); if (config?.requireCommit) {
setVcsClient(new GitClient());
} }
Log.debug(err);
throw new Error(baseMessage);
} }
} }
@@ -1,20 +1,20 @@
import { CombinedError } from '@urql/core'; import { flushAsync, initAsync, logEvent } from '../../analytics/rudderstackClient';
import { jester as mockJester } from '../../credentials/__tests__/fixtures-constants';
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager'; import { getUserAsync } from '../../user/User';
import Log from '../../log'; import { ensureLoggedInAsync } from '../../user/actions';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand'; import EasCommand from '../EasCommand';
jest.mock('../../user/User'); jest.mock('../../user/User');
jest.mock('../../user/SessionManager'); jest.mock('../../user/actions', () => ({ ensureLoggedInAsync: jest.fn() }));
jest.mock('../../analytics/AnalyticsManager', () => { jest.mock('../../analytics/rudderstackClient', () => {
const { CommandEvent } = jest.requireActual('../../analytics/AnalyticsManager'); const { AnalyticsEvent } = jest.requireActual('../../analytics/rudderstackClient');
return { return {
CommandEvent, AnalyticsEvent,
createAnalyticsAsync: jest.fn(), logEvent: jest.fn(),
initAsync: jest.fn(),
flushAsync: jest.fn(),
}; };
}); });
jest.mock('../../log');
let originalProcessArgv: string[]; let originalProcessArgv: string[];
@@ -27,24 +27,28 @@ afterAll(() => {
process.argv = originalProcessArgv; process.argv = originalProcessArgv;
}); });
const analytics: AnalyticsWithOrchestration = {
logEvent: jest.fn((): void => {}),
setActor: jest.fn((): void => {}),
flushAsync: jest.fn(async (): Promise<void> => {}),
};
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest
.mocked(getUserAsync)
jest.mocked(createAnalyticsAsync).mockResolvedValue(analytics); .mockReset()
.mockImplementation(async () => mockJester);
jest
.mocked(ensureLoggedInAsync)
.mockReset()
.mockImplementation(async () => mockJester);
jest.mocked(initAsync).mockReset();
jest.mocked(flushAsync).mockReset();
jest.mocked(logEvent).mockReset();
}); });
const createTestEasCommand = (): typeof EasCommand => { const createTestEasCommand = (authValue: boolean): typeof EasCommand => {
class TestEasCommand extends EasCommand { class TestEasCommand extends EasCommand {
requiresAuthentication = authValue;
async runAsync(): Promise<void> {} 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 TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand; return TestEasCommand;
}; };
@@ -57,33 +61,40 @@ describe(EasCommand.name, () => {
// //
// See https://github.com/oclif/command/blob/master/src/command.ts#L80 // See https://github.com/oclif/command/blob/master/src/command.ts#L80
// and look for "Config.load" // and look for "Config.load"
it('ensures the user data is read', async () => { it('ensures the user is logged in', async () => {
const TestEasCommand = createTestEasCommand(); const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run(); await TestEasCommand.run();
const sessionManagerSpy = jest.spyOn(SessionManager.prototype, 'getUserAsync'); expect(ensureLoggedInAsync).toHaveBeenCalled();
expect(sessionManagerSpy).toBeCalledTimes(1); }, 15_000);
}, 30_000);
it('ensures the user data is read from cache', async () => {
const TestEasCommand = createTestEasCommand(false);
await TestEasCommand.run();
expect(jest.mocked(getUserAsync)).toBeCalledTimes(1);
expect(jest.mocked(ensureLoggedInAsync)).not.toBeCalled();
});
it('initializes analytics', async () => { it('initializes analytics', async () => {
const TestEasCommand = createTestEasCommand(); const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run(); await TestEasCommand.run();
expect(createAnalyticsAsync).toHaveBeenCalled(); expect(initAsync).toHaveBeenCalled();
}); });
it('flushes analytics', async () => { it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(); const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run(); await TestEasCommand.run();
expect(analytics.flushAsync).toHaveBeenCalled(); expect(flushAsync).toHaveBeenCalled();
}); });
it('logs events', async () => { it('logs events', async () => {
const TestEasCommand = createTestEasCommand(); const TestEasCommand = createTestEasCommand(true);
await TestEasCommand.run(); await TestEasCommand.run();
expect(analytics.logEvent).toHaveBeenCalledWith('action', { expect(logEvent).toHaveBeenCalledWith('action', {
action: `eas ${TestEasCommand.id}`, action: `eas ${TestEasCommand.id}`,
}); });
}); });
@@ -91,80 +102,14 @@ describe(EasCommand.name, () => {
describe('after exceptions', () => { describe('after exceptions', () => {
it('flushes analytics', async () => { it('flushes analytics', async () => {
const TestEasCommand = createTestEasCommand(); const TestEasCommand = createTestEasCommand(true);
try { try {
await TestEasCommand.run().then(() => { await TestEasCommand.run().then(() => {
throw new Error('foo'); throw new Error('foo');
}); });
} catch {} } catch (error) {}
expect(analytics.flushAsync).toHaveBeenCalled(); expect(flushAsync).toHaveBeenCalled();
});
describe('catch', () => {
it('logs the message', async () => {
const TestEasCommand = createTestEasCommand();
const logErrorSpy = jest.spyOn(Log, 'error');
const logDebugSpy = jest.spyOn(Log, 'debug');
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
const error = new Error('Unexpected, internal error message');
runAsyncMock.mockImplementation(() => {
throw error;
});
try {
await TestEasCommand.run();
} catch {}
expect(logErrorSpy).toBeCalledWith('Unexpected, internal error message');
expect(logDebugSpy).toBeCalledWith(error);
});
it('logs the cleaned message if needed', async () => {
const TestEasCommand = createTestEasCommand();
const logErrorSpy = jest.spyOn(Log, 'error');
const logDebugSpy = jest.spyOn(Log, 'debug');
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
const graphQLErrors = ['Unexpected GraphQL error message'];
const error = new CombinedError({ graphQLErrors });
runAsyncMock.mockImplementation(() => {
throw error;
});
try {
await TestEasCommand.run();
} catch {}
expect(logErrorSpy).toBeCalledWith('Unexpected GraphQL error message');
expect(logDebugSpy).toBeCalledWith(error);
});
it('re-throws the error with default base message', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
throw new Error('Error message');
});
try {
await TestEasCommand.run();
} catch (caughtError) {
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError as Error).message).toEqual('testEasCommand command failed.');
}
});
it('re-throws the error with a different default base message in case of graphQLError', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
const graphQLErrors = ['Unexpected GraphQL error message'];
throw new CombinedError({ graphQLErrors });
});
try {
await TestEasCommand.run();
} catch (caughtError) {
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError as Error).message).toEqual('GraphQL request failed.');
}
});
}); });
}); });
}); });
@@ -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>;
}
@@ -1,34 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { ExpoConfigOptions, getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export type DynamicConfigContextFn = (options: ExpoConfigOptions) => Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}>;
export class DynamicProjectConfigContextField extends ContextField<DynamicConfigContextFn> {
async getValueAsync({
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const expBefore = getExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
env: options?.env,
});
const exp = getExpoConfig(projectDir, options);
return {
exp,
projectDir,
projectId,
};
};
}
}
@@ -1,30 +0,0 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class LoggedInContextField extends ContextField<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
const graphqlClient = createGraphqlClient(authenticationInfo);
return {
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -1,32 +0,0 @@
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
export default class MaybeLoggedInContextField extends ContextField<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<{
actor: Actor | null;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
}> {
const authenticationInfo = {
accessToken: sessionManager.getAccessToken(),
sessionSecret: sessionManager.getSessionSecret(),
};
const graphqlClient = createGraphqlClient(authenticationInfo);
const actor = await sessionManager.getUserAsync();
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
return {
actor: actor ?? null,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
};
}
}
@@ -1,49 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export class OptionalProjectConfigContextField extends ContextField<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
| {
projectId: string;
exp: ExpoConfig;
projectDir: string;
}
| undefined
> {
let projectDir: string;
try {
projectDir = await findProjectDirAndVerifyProjectSetupAsync();
if (!projectDir) {
return undefined;
}
} catch (e) {
if (e instanceof InvalidEasJsonError) {
throw e;
}
return undefined;
}
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
exp,
projectDir,
projectId,
};
}
}
@@ -1,31 +0,0 @@
import { ExpoConfig } from '@expo/config-types';
import { getExpoConfig } from '../../project/expoConfig';
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
export default class ProjectConfigContextField extends ContextField<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
projectId: string;
exp: ExpoConfig;
projectDir: string;
}> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
const exp = getExpoConfig(projectDir);
return {
projectId,
exp,
projectDir,
};
}
}
@@ -1,8 +0,0 @@
import ContextField from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
export default class ProjectDirContextField extends ContextField<string> {
async getValueAsync(): Promise<string> {
return await findProjectDirAndVerifyProjectSetupAsync();
}
}
@@ -1,8 +0,0 @@
import SessionManager from '../../user/SessionManager';
import ContextField, { ContextOptions } from './ContextField';
export default class SessionManagementContextField extends ContextField<SessionManager> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<SessionManager> {
return sessionManager;
}
}
@@ -1,62 +0,0 @@
import { vol } from 'memfs';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../prompts');
beforeEach(() => {
jest.resetAllMocks();
});
describe(findProjectRootAsync, () => {
beforeEach(() => {
vol.reset();
});
it('throws if not inside the project directory', async () => {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
await expect(findProjectRootAsync({ cwd: '/app' })).rejects.toThrow(
'Run this command inside a project directory.'
);
});
it('defaults to process.cwd() if defaultToProcessCwd = true', async () => {
const spy = jest.spyOn(process, 'cwd').mockReturnValue('/this/is/fake/process/cwd');
try {
vol.fromJSON(
{
'./README.md': '1',
},
'/app'
);
const projectDir = await findProjectRootAsync({
cwd: '/app',
defaultToProcessCwd: true,
});
expect(projectDir).toBe('/this/is/fake/process/cwd');
} finally {
spy.mockRestore();
}
});
it('returns the root directory of the project', async () => {
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
const projectRoot = await findProjectRootAsync({ cwd: '/app/src' });
expect(projectRoot).toBe('/app');
});
});
@@ -1,277 +0,0 @@
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { anything, instance, mock, when } from 'ts-mockito';
import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import SessionManager from '../../../../user/SessionManager';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync';
jest.mock('@expo/config');
jest.mock('fs');
jest.mock('../../../../graphql/queries/AppQuery');
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
jest.mock('../../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
describe(getProjectIdAsync, () => {
let sessionManager: SessionManager;
beforeEach(() => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'User',
id: 'user_id',
username: 'notnotbrent',
primaryAccount: {
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Owner, actor: { id: 'user_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'user_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
sessionManager = instance(sessionManagerMock);
vol.fromJSON(
{
'./README.md': '1',
'./package.json': '2',
'./src/index.ts': '3',
},
'/app'
);
jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
});
it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).resolves.toEqual('1234');
});
it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (notnotbrent) does not match owner specified in the "owner" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Slug for project identified by "extra.eas.projectId" (test) does not match the "slug" field (wat). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success',
config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '2345');
const projectId = await getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'test' },
{
nonInteractive: false,
}
);
expect(projectId).toEqual('2345');
expect(modifyConfigAsync).toHaveBeenCalledTimes(1);
expect(modifyConfigAsync).toHaveBeenCalledWith(
'/app',
{
extra: { eas: { projectId: '2345' } },
},
{ skipSDKVersionRequirement: true }
);
});
it('throws if writing the ID back to the config fails', async () => {
jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail',
config: null,
});
jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
.mockImplementation(async () => '4567');
await expect(
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
).rejects.toThrow();
});
it('throws if extra.eas.projectId is not a string', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@notnotbrent/test',
slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: "extra.eas.projectId" must be a string, found number. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
);
});
});
@@ -1,64 +0,0 @@
import {
AnyVariables,
Client,
OperationContext,
OperationResult,
PromisifiedSource,
TypedDocumentNode,
cacheExchange,
createClient as createUrqlClient,
dedupExchange,
fetchExchange,
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
import { DocumentNode } from 'graphql';
import fetch from 'node-fetch';
import { getExpoApiBaseUrl } from '../../../api';
import { httpsProxyAgent } from '../../../fetch';
export interface ExpoGraphqlClient extends Client {
query<Data = any, Variables extends AnyVariables = AnyVariables>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): PromisifiedSource<OperationResult<Data, Variables>>;
}
export function createGraphqlClient(authInfo: {
accessToken: string | null;
sessionSecret: string | null;
}): ExpoGraphqlClient {
return createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
exchanges: [
dedupExchange,
cacheExchange,
retryExchange({
maxDelayMs: 4000,
retryIf: (err, operation) => {
return !!(
err &&
!operation.context.noRetry &&
(err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))
);
},
}),
fetchExchange,
],
// @ts-expect-error Type 'typeof fetch' is not assignable to type '(input: RequestInfo, init?: RequestInit | undefined) => Promise<Response>'.
fetch,
fetchOptions: (): RequestInit => {
const headers: Record<string, string> = {};
if (authInfo.accessToken) {
headers.authorization = `Bearer ${authInfo.accessToken}`;
} else if (authInfo.sessionSecret) {
headers['expo-session'] = authInfo.sessionSecret;
}
return {
...(httpsProxyAgent ? { agent: httpsProxyAgent } : {}),
headers,
};
},
}) as ExpoGraphqlClient;
}
@@ -1,118 +0,0 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import * as PackageManagerUtils from '@expo/package-manager';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import pkgDir from 'pkg-dir';
import semver from 'semver';
import { learnMore } from '../../../log';
import { easCliVersion } from '../../../utils/easCli';
import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
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());
}
}
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
let printCliVersionWarning = false;
const consoleWarn = (msg?: string): void => {
if (msg) {
// eslint-disable-next-line no-console
console.warn(chalk.yellow(msg));
} else {
// eslint-disable-next-line no-console
console.warn();
}
};
if (await isEasCliInDependenciesAsync(projectDir)) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
}
const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
}
if (printCliVersionWarning) {
consoleWarn(
`It's recommended to use the ${chalk.bold(
'"cli.version"'
)} field in eas.json to enforce the ${chalk.bold('eas-cli')} version for your project.`
);
consoleWarn(
learnMore('https://github.com/expo/eas-cli#enforcing-eas-cli-version-for-your-project')
);
consoleWarn();
}
}
async function isEasCliInDependenciesAsync(dir: string): Promise<boolean> {
const packageJsonPath = path.join(dir, 'package.json');
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
return (
packageJson?.dependencies?.['eas-cli'] !== undefined ||
packageJson?.devDependencies?.['eas-cli'] !== undefined
);
}
/**
* @returns the project root directory.
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectRootAsync({
cwd,
defaultToProcessCwd = false,
}: {
cwd?: string;
defaultToProcessCwd?: boolean;
} = {}): Promise<string> {
const projectRootDir = await pkgDir(cwd);
if (!projectRootDir) {
if (!defaultToProcessCwd) {
throw new Error('Run this command inside a project directory.');
} else {
return process.cwd();
}
} else {
let vcsRoot;
try {
vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
} catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error(
`package.json is outside of the current git repository (project root: ${projectRootDir}, git root: ${vcsRoot}.`
);
}
return projectRootDir;
}
}
/**
* Determine the project root directory and ensure some constraints about the project setup
* like CLI version and dependencies.
* @returns the project root directory
*
* @deprecated Should not be used outside of context functions.
*/
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync();
await applyCliConfigAsync(projectDir);
await ensureEasCliIsNotInDependenciesAsync(projectDir);
return projectDir;
}
@@ -1,195 +0,0 @@
import { getProjectConfigDescription, modifyConfigAsync } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job';
import chalk from 'chalk';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora';
import { getExpoConfig } from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager';
import { Actor, getActorUsername } from '../../../user/User';
import { createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
/**
* Save an EAS project ID to the appropriate field in the app config.
*
* @deprecated Should not be used outside of context functions except in the init command.
*/
export async function saveProjectIdToAppConfigAsync(
projectDir: string,
projectId: string,
options: { env?: Env } = {}
): Promise<void> {
const exp = getExpoConfig(projectDir, options);
const result = await modifyConfigAsync(
projectDir,
{
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
},
{ skipSDKVersionRequirement: true }
);
switch (result.type) {
case 'success':
break;
case 'warn': {
Log.warn();
Log.warn(
`Warning: Your project uses dynamic app configuration, and the EAS project ID can't automatically be added to it.`
);
Log.warn(
chalk.dim(
'https://docs.expo.dev/workflow/configuration/#dynamic-configuration-with-appconfigjs'
)
);
Log.warn();
Log.warn(
`To complete the setup process, set "${chalk.bold(
'extra.eas.projectId'
)}" in your ${chalk.bold(getProjectConfigDescription(projectDir))}:`
);
Log.warn();
Log.warn(chalk.bold(JSON.stringify({ expo: { extra: { eas: { projectId } } } }, null, 2)));
Log.warn();
throw new Error(result.message);
}
case 'fail':
throw new Error(result.message);
default:
throw new Error('Unexpected result type from modifyConfigAsync');
}
}
/**
* Get the EAS project ID from the app config. If the project ID is not set in the config,
* use the owner/slug to identify an EAS project on the server (asking for confirmation first),
* and attempt to save the EAS project ID to the appropriate field in the app config. If unable to
* save to the app config, throw an error.
*
* @returns the EAS project ID
*
* @deprecated Should not be used outside of context functions.
*/
export async function getProjectIdAsync(
sessionManager: SessionManager,
exp: ExpoConfig,
options: { env?: Env; nonInteractive: boolean }
): Promise<string> {
// all codepaths in this function require a logged-in user with access to the owning account
// since they either query the app via graphql or create it, which includes getting info about
// the owner
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive: options.nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) {
if (typeof localProjectId !== 'string') {
throw new Error(
`Project config: "extra.eas.projectId" must be a string, found ${typeof localProjectId}. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
);
}
// check that the local project ID matches account and slug
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, localProjectId);
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match owner specified in the "owner" field (${exp.owner}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
}
if (exp.slug && exp.slug !== appForProjectId.slug) {
throw new Error(
`Project config: Slug for project identified by "extra.eas.projectId" (${
appForProjectId.slug
}) does not match the "slug" field (${exp.slug}). ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
return localProjectId;
}
const projectDir = await findProjectRootAsync();
if (!projectDir) {
throw new Error('This command must be run inside a project directory.');
}
Log.warn('EAS project not configured.');
const getAccountNameForEASProjectSync = (exp: ExpoConfig, user: Actor): string => {
if (exp.owner) {
return exp.owner;
}
switch (user.__typename) {
case 'User':
return user.username;
case 'Robot':
throw new Error(
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
);
case 'SSOUser':
throw new Error('SSO users are not supported yet.');
}
};
const projectId = await fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync(
graphqlClient,
{
accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
},
{
nonInteractive: options.nonInteractive,
},
actor
);
const spinner = ora(`Linking local project to EAS project ${projectId}`).start();
try {
await saveProjectIdToAppConfigAsync(projectDir, projectId, options);
spinner.succeed(`Linked local project to EAS project ${projectId}`);
} catch (e: any) {
spinner.fail();
throw e;
}
return projectId;
}
@@ -1,7 +0,0 @@
export class EasCommandError extends Error {
// constructor is not useless, since the constructor for Error allows for optional `message`
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(message: string) {
super(message);
}
}
Loaded 100 of 625 files, more files were not shown because too many files have changed in this diff. Show more