Compare commits

..
Author SHA1 Message Date
Will Schurman a3655c7407 [eas-cli] Add support for asset excludes 2023-02-23 11:24:37 -08:00
91 changed files with 1762 additions and 6224 deletions

No files matched your search

+1 -1
View File
@@ -37,7 +37,7 @@ body:
required: true
attributes:
label: Environment
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
- type: textarea
validations:
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==
+2 -2
View File
@@ -199,9 +199,9 @@ jobs:
env:
SLACK_CHANNEL: eas-cli
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_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'EAS CLI CI'
SLACK_USERNAME: 'Expo CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
-12
View File
@@ -39,15 +39,3 @@ jobs:
if: ${{ matrix.coverage }}
with:
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 -79
View File
@@ -8,89 +8,12 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add support for EAS Update asset excludes. ([#1526](https://github.com/expo/eas-cli/pull/1526) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
### 🧹 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "3.9.3",
"version": "3.6.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+54 -79
View File
@@ -79,7 +79,6 @@ eas --help COMMAND
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
@@ -131,7 +130,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -148,7 +147,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -165,7 +164,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -179,7 +178,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -229,7 +228,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -250,7 +249,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -270,7 +269,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -290,7 +289,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -313,7 +312,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -343,7 +342,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -360,7 +359,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -377,7 +376,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -412,7 +411,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -449,7 +448,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -475,7 +474,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -499,7 +498,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/run.ts)_
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -546,7 +545,7 @@ DESCRIPTION
Update version of an app.
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -565,7 +564,7 @@ DESCRIPTION
Update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -582,7 +581,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -603,7 +602,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -625,7 +624,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -645,31 +644,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
Roll a new branch out on a channel incrementally.
```
USAGE
$ eas channel:rollout [CHANNEL] [--branch <value>] [--percent <value>] [--end] [--json --non-interactive]
ARGUMENTS
CHANNEL channel on which the rollout should be done
FLAGS
--branch=<value> branch to rollout
--end end the rollout
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--percent=<value> percent of users to send to the new branch
DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:view [NAME]`
@@ -692,7 +667,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -713,7 +688,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -730,7 +705,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -744,7 +719,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -764,7 +739,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -785,7 +760,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:view [UDID]`
@@ -799,7 +774,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -813,7 +788,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -901,7 +876,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -918,7 +893,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -935,7 +910,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -949,7 +924,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -963,7 +938,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -985,7 +960,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1008,7 +983,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1026,7 +1001,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1040,7 +1015,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1060,7 +1035,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1090,7 +1065,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1122,7 +1097,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1140,7 +1115,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1161,7 +1136,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1183,11 +1158,11 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
roll back to an existing update
rollback to an existing update
```
USAGE
@@ -1204,10 +1179,10 @@ FLAGS
--non-interactive Run the command in non-interactive mode.
DESCRIPTION
roll back to an existing update
rollback to an existing update
```
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/republish.ts)_
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:view GROUPID`
@@ -1227,7 +1202,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1248,7 +1223,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1268,7 +1243,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1286,7 +1261,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1308,7 +1283,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1325,7 +1300,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
+656 -2772
View File
File diff suppressed because it is too large. Load diff
+4 -5
View File
@@ -1,20 +1,20 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "3.9.3",
"version": "3.6.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.2.0",
"@expo/apple-utils": "0.0.0-alpha.37",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "7.0.3",
"@expo/config-plugins": "5.0.4",
"@expo/config-types": "47.0.0",
"@expo/eas-build-job": "1.0.7",
"@expo/eas-json": "3.9.2",
"@expo/eas-build-job": "0.2.103",
"@expo/eas-json": "3.6.1",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
@@ -27,7 +27,6 @@
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.8",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.23.2",
"@oclif/plugin-autocomplete": "1.3.10",
@@ -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 -2
View File
@@ -124,8 +124,7 @@ export async function prepareAndroidBuildAsync(
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
const isCustomBuild = ctx.buildProfile.config !== undefined;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
return !ctx.buildProfile.withoutCredentials;
}
async function ensureAndroidCredentialsAsync(
@@ -1,12 +1,7 @@
import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -27,8 +22,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
buildType: job.buildType && transformBuildType(job.buildType),
developmentClient: job.developmentClient,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
};
}
@@ -12,7 +12,6 @@ import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -25,6 +24,7 @@ interface JobData {
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
@@ -49,15 +49,11 @@ export async function prepareJobAsync(
}
: {};
let buildType = buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') {
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
buildType = Android.BuildType.APK;
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
@@ -79,8 +75,8 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
@@ -93,15 +89,10 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
mode: BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
+24 -29
View File
@@ -10,7 +10,6 @@ import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import {
AppPlatform,
BuildFragment,
@@ -37,15 +36,6 @@ import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
TurtleDeprecatedJobFormatError,
} from './errors';
import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
@@ -171,33 +161,38 @@ export async function prepareBuildRequestForPlatformAsync<
};
}
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
VALIDATION_ERROR: RequestValidationError,
};
const SERVER_SIDE_DEFINED_ERRORS = [
'TURTLE_DEPRECATED_JOB_FORMAT',
'EAS_BUILD_FREE_TIER_DISABLED',
'EAS_BUILD_FREE_TIER_DISABLED_IOS',
'EAS_BUILD_FREE_TIER_DISABLED_ANDROID',
'VALIDATION_ERROR',
];
export function handleBuildRequestError(error: any, platform: Platform): never {
function handleBuildRequestError(error: any, platform: Platform): never {
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(
if (SERVER_SIDE_DEFINED_ERRORS.includes(error?.graphQLErrors?.[0]?.extensions?.errorCode)) {
Log.error(error?.graphQLErrors?.[0]?.message);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
`EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`
);
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
throw new EasBuildTooManyPendingBuildsError(
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
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.'
);
}
@@ -612,7 +607,7 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
}
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await easJsonAccessor.readRawJsonAsync();
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
@@ -620,7 +615,7 @@ async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Pr
for (const profileName of profileNames) {
easJsonRawObject.build[profileName].ios = {
...easJsonRawObject.build[profileName].ios,
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
};
}
return easJsonRawObject;
+6 -6
View File
@@ -54,18 +54,18 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
developmentClient: true,
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
},
@@ -86,18 +86,18 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
ios: {
buildConfiguration: 'Debug',
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M_MEDIUM,
resourceClass: ResourceClass.M1_MEDIUM,
},
},
},
-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 {}
+2 -7
View File
@@ -91,16 +91,11 @@ export function transformIosEnterpriseProvisioning(
}
}
// TODO: check what in metadata
export function transformBuildMode(buildMode: string): BuildMode {
export function transformBuildMode(buildMode: Metadata['buildMode']): 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}`);
return BuildMode.Resign;
}
}
@@ -55,6 +55,5 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
return !buildCtx.buildProfile.simulator;
}
+2 -9
View File
@@ -2,12 +2,7 @@ import { Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -17,7 +12,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
secrets: transformIosSecrets(job.secrets),
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
@@ -29,8 +24,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
developmentClient: job.developmentClient,
simulator: job.simulator,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
};
}
+22 -32
View File
@@ -7,14 +7,12 @@ import {
Platform,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows';
import path from 'path';
import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
@@ -28,17 +26,17 @@ interface JobData {
const cacheDefaults = {
disabled: false,
customPaths: [],
cacheDefaultPaths: true,
};
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
const username = getUsername(ctx.exp, ctx.user);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
@@ -46,41 +44,38 @@ export async function prepareJobAsync(
}
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
projectArchive: jobData.projectArchive,
projectRootDirectory,
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
image: ctx.buildProfile.image,
node: ctx.buildProfile.node,
yarn: ctx.buildProfile.yarn,
bundler: ctx.buildProfile.bundler,
cocoapods: ctx.buildProfile.cocoapods,
fastlane: ctx.buildProfile.fastlane,
expoCli: ctx.buildProfile.expoCli,
env: ctx.buildProfile.env,
},
cache: {
...cacheDefaults,
...buildProfile.cache,
...ctx.buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
buildConfiguration: ctx.buildProfile.buildConfiguration,
applicationArchivePath:
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -88,15 +83,10 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
prebuildCommand: ctx.buildProfile.prebuildCommand,
},
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
mode: BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
}
+1 -2
View File
@@ -1,5 +1,5 @@
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 fs from 'fs-extra';
import resolveFrom from 'resolve-from';
@@ -57,7 +57,6 @@ export async function collectMetadataAsync<T extends Platform>(
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
};
return sanitizeMetadata(metadata);
}
@@ -31,7 +31,6 @@ import {
appPlatformEmojis,
toPlatforms,
} from '../platform';
import { validateCustomBuildConfigAsync } from '../project/customBuildConfig';
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
@@ -99,7 +98,7 @@ export async function runBuildAndSubmitAsync(
projectDir,
nonInteractive: flags.nonInteractive,
});
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
@@ -119,7 +118,6 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
}
const startedBuilds: {
@@ -57,6 +57,7 @@ export function printLogsUrls(builds: BuildFragment[]): void {
}
export function printBuildResults(builds: (BuildFragment | null)[]): void {
Log.newLine();
if (builds.length === 1) {
const [build] = builds;
assert(build, 'Build should be defined');
@@ -67,8 +68,8 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
}
function printBuildResult(build: BuildFragment): void {
Log.addNewLineIfNone();
if (build.status === BuildStatus.Errored) {
Log.addNewLineIfNone();
const userError = build.error;
Log.error(
`${appPlatformEmojis[build.platform]} ${
@@ -81,7 +82,6 @@ function printBuildResult(build: BuildFragment): void {
return;
}
if (build.status === BuildStatus.Canceled) {
Log.addNewLineIfNone();
Log.error(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
@@ -91,7 +91,6 @@ function printBuildResult(build: BuildFragment): void {
}
if (build.distribution === DistributionType.Internal) {
Log.addNewLineIfNone();
const logsUrl = getBuildLogsUrl(build);
// 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
@@ -109,14 +108,9 @@ function printBuildResult(build: BuildFragment): void {
} else {
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
// we should account for this case better if it is possible
const url = build.artifacts?.buildUrl;
if (url) {
Log.addNewLineIfNone();
Log.log(
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
);
Log.log(link(url));
}
const url = build.artifacts?.buildUrl ?? '';
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.log(`${link(url)}`);
}
}
@@ -8,30 +8,24 @@ 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.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosM1Medium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosM1Large,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
};
const androidResourceClassToBuildResourceClassMapping: Record<
AndroidResourceClass,
Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>,
BuildResourceClass
> = {
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
@@ -76,7 +70,15 @@ function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): Bui
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
return androidResourceClassToBuildResourceClassMapping[resourceClass as AndroidResourceClass];
return androidResourceClassToBuildResourceClassMapping[
resourceClass as Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>
];
}
async function resolveIosResourceClassAsync(
@@ -87,20 +89,8 @@ async function resolveIosResourceClassAsync(
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.`
);
if (resourceClass === ResourceClass.M1_EXPERIMENTAL) {
Log.warn(`Resource class ${chalk.bold('m1-experimental')} is deprecated.`);
}
return iosResourceClassToBuildResourceClassMapping[resourceClass];
@@ -116,7 +106,7 @@ async function resolveIosDefaultRequestedResourceClassAsync(
(sdkVersion && semver.satisfies(sdkVersion, '>=48')) ||
(reactNativeVersion && semver.satisfies(reactNativeVersion, '>=0.71.0'))
) {
return ResourceClass.M_MEDIUM;
return ResourceClass.M1_MEDIUM;
} else {
return ResourceClass.DEFAULT;
}
+10 -32
View File
@@ -1,5 +1,4 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import nullthrows from 'nullthrows';
import {
@@ -7,7 +6,6 @@ import {
CommandEvent,
createAnalyticsAsync,
} from '../analytics/AnalyticsManager';
import Log from '../log';
import SessionManager from '../user/SessionManager';
import AnalyticsContextField from './context/AnalyticsContextField';
import ContextField from './context/ContextField';
@@ -18,7 +16,6 @@ import { OptionalProjectConfigContextField } from './context/OptionalProjectConf
import ProjectConfigContextField from './context/ProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import { EasCommandError } from './errors';
type ContextInput<
T extends {
@@ -36,8 +33,6 @@ type ContextOutput<
[P in keyof T]: T[P];
};
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = {
/**
@@ -108,18 +103,6 @@ export default abstract class EasCommand extends Command {
*/
static contextDefinition: ContextInput = {};
/**
* 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.
*/
@@ -149,10 +132,20 @@ export default abstract class EasCommand extends Command {
return Object.fromEntries(contextValuePairs);
}
/**
* The user session manager. Responsible for coordinating all user session related state.
* If needed in a subclass, use the SessionManager ContextOption.
*/
private sessionManagerInternal?: SessionManager;
private get sessionManager(): SessionManager {
return nullthrows(this.sessionManagerInternal);
}
/**
* The analytics manager. Used for logging analytics.
* It is set up here to ensure a consistent set up.
*/
private analyticsInternal?: AnalyticsWithOrchestration;
private get analytics(): AnalyticsWithOrchestration {
return nullthrows(this.analyticsInternal);
}
@@ -182,19 +175,4 @@ export default abstract class EasCommand extends Command {
await this.analytics.flushAsync();
return super.finally(err);
}
protected override catch(err: Error): Promise<any> {
let baseMessage = `${this.id} command failed.`;
if (err instanceof EasCommandError) {
Log.error(err.message);
} else if (err instanceof CombinedError && err?.graphQLErrors) {
const cleanMessage = err.message.replace('[GraphQL] ', '');
Log.error(cleanMessage);
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
} else {
Log.error(err.message);
}
Log.debug(err);
throw new Error(baseMessage);
}
}
@@ -1,7 +1,4 @@
import { CombinedError } from '@urql/core';
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
import Log from '../../log';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand';
@@ -14,7 +11,6 @@ jest.mock('../../analytics/AnalyticsManager', () => {
createAnalyticsAsync: jest.fn(),
};
});
jest.mock('../../log');
let originalProcessArgv: string[];
@@ -44,7 +40,7 @@ const createTestEasCommand = (): typeof EasCommand => {
async runAsync(): Promise<void> {}
}
TestEasCommand.id = 'testEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
TestEasCommand.id = 'TestEasCommand'; // normally oclif will assign ids, but b/c this is located outside the commands folder it will not
return TestEasCommand;
};
@@ -100,71 +96,5 @@ describe(EasCommand.name, () => {
expect(analytics.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.');
}
});
});
});
});
@@ -16,8 +16,8 @@ export class DynamicProjectConfigContextField extends ContextField<DynamicConfig
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = getExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
@@ -252,26 +252,4 @@ describe(getProjectIdAsync, () => {
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.`
);
});
});
@@ -12,7 +12,7 @@ import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error(
@@ -89,12 +89,6 @@ export async function getProjectIdAsync(
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) {
@@ -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);
}
}
+1 -1
View File
@@ -251,7 +251,7 @@ export async function handleDeprecatedEasJsonAsync(
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
return;
}
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
[Platform.ANDROID, profileName],
@@ -6,8 +6,6 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
import EasCommand from '../../commandUtils/EasCommand';
import { RequestedPlatform } from '../../platform';
import { enableJsonOutput } from '../../utils/json';
import { setVcsClient } from '../../vcs';
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
/**
* This command will be run on the EAS Build workers, when building
@@ -53,8 +51,6 @@ export default class BuildInternal extends EasCommand {
nonInteractive: true,
});
setVcsClient(new GitNoCommitClient());
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await runBuildAndSubmitAsync(
@@ -121,7 +121,7 @@ export default class BuildResign extends EasCommand {
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
@@ -54,7 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
});
const platform = await selectPlatformAsync(flags.platform);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const profile = await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
@@ -71,7 +71,7 @@ export default class BuildVersionSyncView extends EasCommand {
});
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
const platforms = toPlatforms(requestedPlatform);
@@ -1,8 +1,6 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
@@ -134,11 +132,13 @@ async function startRolloutAsync(
);
}
const logMessage = `Started a rollout of branch ${chalk.bold(branchName)} on channel ${chalk.bold(
channelName!
)}! ${chalk.bold(percent)}% of users will be directed to branch ${chalk.bold(
const logMessage = `️Started a rollout of branch ${chalk.bold(
branchName
)}, ${chalk.bold(100 - percent)}% to branch ${chalk.bold(oldBranch.name)}.`;
)} onto channel ${chalk.bold(channelName!)}! ${chalk.bold(
percent
)}% of users will be directed to branch ${chalk.bold(branchName)}, ${chalk.bold(
100 - percent
)}% to branch ${chalk.bold(oldBranch.name)}.`;
return { newChannelInfo, logMessage };
}
@@ -190,7 +190,7 @@ async function editRolloutAsync(
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `Rollout of branch ${chalk.bold(newBranch.name)} on channel ${chalk.bold(
const logMessage = `️Rollout of branch ${chalk.bold(newBranch.name)} onto channel ${chalk.bold(
channelName!
)} updated from ${chalk.bold(currentPercent)}% to ${chalk.bold(percent)}%. ${chalk.bold(
percent
@@ -256,11 +256,11 @@ async function endRolloutAsync(
'Ending the rollout will send all traffic to a single branch. Which one should that be?',
[
{
title: `${newBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
title: `${newBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
value: true,
},
{
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
value: false,
},
]
@@ -284,7 +284,7 @@ async function endRolloutAsync(
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `Rollout on channel ${chalk.bold(
const logMessage = `️Rollout on channel ${chalk.bold(
channelName
)} ended. All traffic is now sent to branch ${chalk.bold(
endOnNewBranch ? newBranch.name : oldBranch.name
@@ -294,12 +294,14 @@ async function endRolloutAsync(
}
export default class ChannelRollout extends EasCommand {
static override description = 'Roll a new branch out on a channel incrementally.';
static override hidden = true;
static override description = 'Rollout a new branch out to a channel incrementally.';
static override args = [
{
name: 'channel',
description: 'channel on which the rollout should be done',
required: true,
description: 'rollout that the channel is on',
},
];
@@ -309,7 +311,7 @@ export default class ChannelRollout extends EasCommand {
required: false,
}),
percent: Flags.integer({
description: 'percent of users to send to the new branch',
description: 'percent of traffic to redirect to the new branch',
required: false,
}),
end: Flags.boolean({
@@ -326,7 +328,7 @@ export default class ChannelRollout extends EasCommand {
async runAsync(): Promise<void> {
const {
args: { channel: channelNameArg },
args: { channel: channelName },
flags: {
json: jsonFlag,
end: endFlag,
@@ -347,23 +349,9 @@ export default class ChannelRollout extends EasCommand {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
let channelName: string = channelNameArg;
if (!channelName) {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel on which to perform a rollout',
paginatedQueryOptions: {
json: jsonFlag,
nonInteractive,
offset: 0,
},
});
channelName = name;
}
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
channelName: channelName!,
});
if (!channel) {
throw new Error(
@@ -408,15 +396,7 @@ export default class ChannelRollout extends EasCommand {
if (!isRollout) {
rolloutMutationResult = await startRolloutAsync(graphqlClient, {
channelName,
branchName:
branchName ??
(await promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json: jsonFlag,
})),
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
percent,
nonInteractive,
projectId,
@@ -453,36 +433,18 @@ export default class ChannelRollout extends EasCommand {
}
}
async function promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
channelName: string;
nonInteractive: boolean;
json: boolean;
}): Promise<string> {
async function promptForBranchNameAsync(
channelName: string,
nonInteractive: boolean
): Promise<string> {
if (nonInteractive) {
throw new Error('Must supply branch flag in non-interactive mode');
}
const { name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like roll out on ${channelName}?`,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json,
nonInteractive,
offset: 0,
},
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: `Select a branch to rollout onto ${channelName}`,
validate: value => (value ? true : 'A branch must be specified.'),
});
return branchName;
return name;
}
+1 -1
View File
@@ -45,7 +45,7 @@ export default class Config extends EasCommand {
nonInteractive: false,
});
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
const accessor = new EasJsonAccessor(projectDir);
const profileName =
maybeProfile ??
(await selectAsync(
@@ -44,7 +44,7 @@ export default class MetadataLint extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
@@ -46,7 +46,7 @@ export default class MetadataPull extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
@@ -44,7 +44,7 @@ export default class MetadataPush extends EasCommand {
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
});
+1 -1
View File
@@ -115,7 +115,7 @@ export default class Submit extends EasCommand {
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
const submissionProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
easJsonAccessor: new EasJsonAccessor(projectDir),
platforms,
profileName: flagsWithPlatform.profile,
});
@@ -1,14 +1,6 @@
import {
AppJSONConfig,
ExpoConfig,
GetConfigOptions,
PackageJSONConfig,
Platform,
getConfig,
} from '@expo/config';
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import nullthrows from 'nullthrows';
import path from 'path';
import { instance, mock } from 'ts-mockito';
@@ -37,7 +29,6 @@ const updateStub: UpdateFragment = {
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
@@ -67,10 +58,7 @@ jest.mock('../../../project/publish', () => ({
}));
describe(UpdatePublish.name, () => {
afterEach(() => {
vol.reset();
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockClear();
});
afterEach(() => vol.reset());
// Deprecated and split to a new command: update:republish
it('errors with --republish', async () => {
@@ -92,7 +80,7 @@ describe(UpdatePublish.name, () => {
mockTestProject();
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
});
@@ -148,59 +136,14 @@ describe(UpdatePublish.name, () => {
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a new update with the public expo config', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
// Add configuration to the project that should not be included in the update
const { appJson } = mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdatePublish(flags, commandOptions).run();
// Pull the publish data from the mocked publish function
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
// Pull the Expo config from the publish data
const expoConfig = nullthrows(publishData.updateInfoGroup).ios!.extra.expoClient;
// Ensure the basic properties are present
expect(expoConfig).toHaveProperty('name', appJson.expo.name);
expect(expoConfig).toHaveProperty('slug', appJson.expo.slug);
// Ensure non-public config is not present
expect(expoConfig).not.toHaveProperty('hooks');
});
});
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
function mockTestProject({
configuredProjectId = '1234',
expoConfig = {},
}: {
configuredProjectId?: string;
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
} = {}): { projectId: string } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
@@ -220,7 +163,6 @@ function mockTestProject({
projectId: configuredProjectId,
},
},
...expoConfig,
},
};
@@ -238,27 +180,11 @@ function mockTestProject({
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async (options?: GetConfigOptions) => {
let exp = { ...mockManifest.exp };
// Fake a limited public config
if (options?.isPublicConfig) {
exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
}
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
.mockResolvedValue(async () => ({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
}));
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
@@ -273,7 +199,7 @@ function mockTestProject({
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId, appJson: appJSON };
return { projectId: configuredProjectId };
}
/** Create a new in-memory export of the project */
@@ -299,8 +225,8 @@ function mockTestExport({
);
}
jest.mocked(collectAssetsAsync).mockResolvedValue(
Object.fromEntries(
jest.mocked(collectAssetsAsync).mockResolvedValue({
collectedAssets: new Map(
platforms.map(platform => [
platform,
{
@@ -311,8 +237,9 @@ function mockTestExport({
},
},
])
)
);
),
excludedAssets: [],
});
jest.mocked(uploadAssetsAsync).mockResolvedValue({
// platforms are mocked all containing only the launch asset
@@ -27,7 +27,6 @@ const updateStub: UpdateFragment = {
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
@@ -1,257 +0,0 @@
import {
AppJSONConfig,
ExpoConfig,
GetConfigOptions,
PackageJSONConfig,
getConfig,
} from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import nullthrows from 'nullthrows';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import { ensureBranchExistsAsync } from '../../../branch/queries';
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
const projectRoot = '/test-project';
const commandOptions = { root: projectRoot } as any;
const updateStub: UpdateFragment = {
id: 'update-1234',
group: 'group-1234',
branch: { id: 'branch-1234', name: 'main' },
message: 'test message',
runtimeVersion: 'exposdk:47.0.0',
platform: 'ios',
gitCommitHash: 'commit',
manifestFragment: JSON.stringify({ fake: 'manifest' }),
isRollBackToEmbedded: false,
manifestPermalink: 'https://expo.dev/fake/manifest/link',
codeSigningInfo: null,
createdAt: '2022-01-01T12:00:00Z',
};
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
...jest.requireActual('../../../project/publish'),
buildBundlesAsync: jest.fn(),
collectAssetsAsync: jest.fn(),
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
uploadAssetsAsync: jest.fn(),
}));
describe(UpdateRollBackToEmbedded.name, () => {
afterEach(() => vol.reset());
it('errors with both --channel and --branch', async () => {
const flags = ['--channel=channel123', '--branch=branch123'];
mockTestProject();
await expect(new UpdateRollBackToEmbedded(flags, commandOptions).run()).rejects.toThrow(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
);
});
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
const { projectId } = mockTestProject();
const platforms = ['android', 'ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
platforms.map(platform => ({
...updateStub,
runtimeVersion,
platform,
}))
);
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
expect.any(Object), // graphql client
{
appId: projectId,
branchName: 'branchFromChannel',
}
);
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
});
it('creates a roll back to embedded with the public expo config', async () => {
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
// Add configuration to the project that should not be included in the update
mockTestProject({
expoConfig: {
hooks: {
postPublish: [
{
file: 'custom-hook.js',
config: { some: 'config' },
},
],
},
},
});
const platforms = ['ios'];
const runtimeVersion = 'exposdk:47.0.0';
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
// Mock an existing branch, so we don't create a new one
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
branchId: 'branch123',
createdBranch: false,
});
jest
.mocked(PublishMutation.publishUpdateGroupAsync)
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
// Pull the publish data from the mocked publish function
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
expect(nullthrows(publishData.rollBackToEmbeddedInfoGroup).ios).toBe(true);
});
});
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
function mockTestProject({
configuredProjectId = '1234',
expoConfig = {},
}: {
configuredProjectId?: string;
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
description: 'fake description',
main: 'index.js',
};
const appJSON: AppJSONConfig = {
expo: {
name: 'testing 123',
version: '0.1.0',
slug: 'testing-123',
sdkVersion: '33.0.0',
owner: jester.accounts[0].name,
extra: {
eas: {
projectId: configuredProjectId,
},
},
...expoConfig,
},
};
vol.fromJSON(
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
},
projectRoot
);
const mockManifest = { exp: appJSON.expo };
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async (options?: GetConfigOptions) => {
let exp = { ...mockManifest.exp };
// Fake a limited public config
if (options?.isPublicConfig) {
exp = {
name: mockManifest.exp.name,
version: mockManifest.exp.version,
slug: mockManifest.exp.slug,
sdkVersion: mockManifest.exp.sdkVersion,
owner: mockManifest.exp.owner,
extra: mockManifest.exp.extra,
};
}
return {
exp,
projectDir: projectRoot,
projectId: configuredProjectId,
};
});
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
actor: jester,
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
graphqlClient,
});
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
slug: 'testing-123',
fullName: '@jester/testing-123',
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId, appJson: appJSON };
}
+175 -51
View File
@@ -1,9 +1,13 @@
import { Platform as PublishPlatform } from '@expo/config';
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { Errors, Flags } from '@oclif/core';
import assert from 'assert';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
import { getDefaultBranchNameAsync } from '../../branch/utils';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
@@ -19,32 +23,32 @@ import {
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { learnMore, link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
buildBundlesAsync,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
defaultPublishPlatforms,
filterExportedPlatformsByFlag,
getBranchNameForCommandAsync,
getRequestedPlatform,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateGroupJsonInfo } from '../../update/utils';
import { maybeWarnAboutEasPlanOverageAsync } from '../../utils/billing';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import {
formatUpdateMessage,
getUpdateGroupJsonInfo,
truncateString as truncateUpdateMessage,
} from '../../update/utils';
import {
checkManifestBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getManifestBodyAsync,
signBody,
signManifestBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
@@ -52,6 +56,8 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
type RawUpdateFlags = {
auto: boolean;
branch?: string;
@@ -82,6 +88,21 @@ type UpdateFlags = {
nonInteractive: boolean;
};
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
switch (platform) {
case 'android':
return RequestedPlatform.Android;
case 'ios':
return RequestedPlatform.Ios;
case 'web':
return null;
case 'all':
return RequestedPlatform.All;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
export default class UpdatePublish extends EasCommand {
static override description = 'publish an update group';
@@ -145,18 +166,18 @@ export default class UpdatePublish extends EasCommand {
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdatePublish);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
const {
let {
auto: autoFlag,
platform: platformFlag,
channelName: channelNameArg,
updateMessage: updateMessageArg,
channelName,
updateMessage,
inputDir,
skipBundler,
privateKeyPath,
json: jsonFlag,
nonInteractive,
branchName: branchNameArg,
} = this.sanitizeFlags(rawFlags);
let branchName = this.sanitizeFlags(rawFlags).branchName;
const {
getDynamicProjectConfigAsync,
@@ -177,7 +198,10 @@ export default class UpdatePublish extends EasCommand {
isPublicConfig: true,
});
await maybeWarnAboutEasPlanOverageAsync(graphqlClient, projectId);
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
@@ -187,28 +211,75 @@ export default class UpdatePublish extends EasCommand {
projectId,
});
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
const { exp } = await getDynamicProjectConfigAsync({});
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
});
let realizedPlatforms: PublishPlatform[] = [];
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
});
if (channelName && branchName) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
);
}
if (channelName) {
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
}
if (!branchName) {
if (autoFlag) {
branchName = await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
} else {
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to publish on?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
} catch {
// unable to select a branch (network error or no branches for project)
({ name: branchName } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : 'Branch name may not be empty.'),
}));
}
assert(branchName, 'Branch name must be specified.');
}
}
if (!updateMessage && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
({ updateMessage } = await promptAsync({
type: 'text',
name: 'updateMessage',
message: `Provide an update message.`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
}));
}
// build bundle and upload assets for a new publish
if (!skipBundler) {
@@ -229,12 +300,11 @@ export default class UpdatePublish extends EasCommand {
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
let uploadedAssetCount = 0;
let assetLimitPerUpdateGroup = 0;
let realizedPlatforms: PublishPlatform[] = [];
try {
const collectedAssets = await collectAssetsAsync(distRoot);
const { collectedAssets, excludedAssets } = await collectAssetsAsync(projectDir, distRoot);
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
realizedPlatforms = Array.from(assets.keys());
const uploadResults = await uploadAssetsAsync(
graphqlClient,
@@ -260,6 +330,13 @@ export default class UpdatePublish extends EasCommand {
Log.withTick(
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
);
if (excludedAssets.length > 0) {
Log.withTick(
`Excluded ${excludedAssets.length} ${
excludedAssets.length === 1 ? 'asset' : 'assets'
} from upload`
);
}
if (uploadedNormalAssetCount === 0) {
Log.withTick(`Uploading assets skipped - no new assets found`);
} else {
@@ -281,9 +358,22 @@ export default class UpdatePublish extends EasCommand {
throw e;
}
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
}
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
@@ -318,13 +408,14 @@ export default class UpdatePublish extends EasCommand {
branchId,
updateInfoGroup: localUpdateInfoGroup,
runtimeVersion,
message: updateMessage,
message: truncatedMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
@@ -335,10 +426,7 @@ export default class UpdatePublish extends EasCommand {
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.updateInfoGroup)).length
);
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
return {
updateGroup,
newUpdates,
@@ -358,13 +446,11 @@ export default class UpdatePublish extends EasCommand {
checkManifestBodyAgainstUpdateInfoGroup(
manifestBody,
nullthrows(
nullthrows(updateGroup.updateInfoGroup)[
newUpdate.platform as keyof UpdateInfoGroup
]
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
)
);
const manifestSignature = signBody(manifestBody, codeSigningInfo);
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
@@ -426,7 +512,7 @@ export default class UpdatePublish extends EasCommand {
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage },
{ label: 'Message', value: truncatedMessage },
...(gitCommitHash
? [
{
@@ -497,3 +583,41 @@ export default class UpdatePublish extends EasCommand {
};
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platforms: PublishPlatform[],
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
for (const platform of platforms) {
if (platform === 'web') {
continue;
}
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged =
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
if (!isManaged) {
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
);
}
}
}
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
platform,
runtimeVersion: nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
});
}
@@ -56,7 +56,7 @@ type UpdateToRepublish = {
>;
export default class UpdateRepublish extends EasCommand {
static override description = 'roll back to an existing update';
static override description = 'rollback to an existing update';
static override flags = {
channel: Flags.string({
@@ -1,389 +0,0 @@
import { Platform as PublishPlatform } from '@expo/config';
import { Errors, Flags } from '@oclif/core';
import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { ensureBranchExistsAsync } from '../../branch/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import { ensureChannelExistsAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import fetch from '../../fetch';
import {
PublishUpdateGroupInput,
StatuspageServiceName,
UpdatePublishMutation,
} from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
defaultPublishPlatforms,
getBranchNameForCommandAsync,
getRequestedPlatform,
getRuntimeToPlatformMappingFromRuntimeVersions,
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
} from '../../project/publish';
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
import { getUpdateGroupJsonInfo } from '../../update/utils';
import {
CodeSigningInfo,
checkDirectiveBodyAgainstUpdateInfoGroup,
getCodeSigningInfoAsync,
getDirectiveBodyAsync,
signBody,
} from '../../utils/code-signing';
import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
type RawUpdateFlags = {
auto: boolean;
branch?: string;
channel?: string;
message?: string;
platform: string;
'private-key-path'?: string;
'non-interactive': boolean;
json: boolean;
};
type UpdateFlags = {
auto: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
updateMessage?: string;
privateKeyPath?: string;
json: boolean;
nonInteractive: boolean;
};
export default class UpdateRollBackToEmbedded extends EasCommand {
static override hidden = true; // until we launch
static override description = 'roll back to the embedded update';
static override flags = {
branch: Flags.string({
description: 'Branch to publish the rollback to embedded update group on',
required: false,
}),
channel: Flags.string({
description: 'Channel that the published rollback to embedded update should affect',
required: false,
}),
message: Flags.string({
description: 'A short message describing the rollback to embedded update',
required: false,
}),
platform: Flags.enum({
char: 'p',
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
auto: Flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
default: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.LoggedIn,
};
async runAsync(): Promise<void> {
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
const {
auto: autoFlag,
platform: platformFlag,
channelName: channelNameArg,
updateMessage: updateMessageArg,
privateKeyPath,
json: jsonFlag,
nonInteractive,
branchName: branchNameArg,
} = this.sanitizeFlags(rawFlags);
const {
getDynamicProjectConfigAsync,
loggedIn: { graphqlClient },
} = await this.getContextAsync(UpdateRollBackToEmbedded, {
nonInteractive,
});
if (jsonFlag) {
enableJsonOutput();
}
const {
exp: expPossiblyWithoutEasUpdateConfigured,
projectId,
projectDir,
} = await getDynamicProjectConfigAsync({
isPublicConfig: true,
});
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp: expPossiblyWithoutEasUpdateConfigured,
platform: getRequestedPlatform(platformFlag),
projectDir,
projectId,
});
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
isPublicConfig: false,
});
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
});
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
});
const realizedPlatforms: PublishPlatform[] =
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
appId: projectId,
branchName,
});
if (createdBranch) {
await ensureChannelExistsAsync(graphqlClient, {
appId: projectId,
branchId,
channelName: branchName,
});
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
try {
newUpdates = await this.publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
realizedPlatforms,
});
publishSpinner.succeed('Published!');
} catch (e) {
publishSpinner.fail('Failed to publish updates');
throw e;
}
if (jsonFlag) {
printJsonOnlyOutput(getUpdateGroupJsonInfo(newUpdates));
} else {
if (new Set(newUpdates.map(update => update.group)).size > 1) {
Log.addNewLineIfNone();
Log.log(
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
);
}
Log.addNewLineIfNone();
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
const newUpdatesForRuntimeVersion = newUpdates.filter(
update => update.runtimeVersion === runtime.runtimeVersion
);
if (newUpdatesForRuntimeVersion.length === 0) {
throw new Error(
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
);
}
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
update => update.platform === 'android'
);
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
const projectName = exp.slug;
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
const updateGroupLink = link(updateGroupUrl, { dim: false });
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: runtime.runtimeVersion },
{ label: 'Platform', value: platforms.join(', ') },
{ label: 'Update group ID', value: updateGroupId },
...(newAndroidUpdate
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
: []),
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
{ label: 'Message', value: updateMessage },
...(gitCommitHash
? [
{
label: 'Commit',
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
},
]
: []),
{ label: 'Website link', value: updateGroupLink },
])
);
Log.addNewLineIfNone();
}
}
}
private async publishRollbacksAsync({
graphqlClient,
isGitWorkingTreeDirty,
gitCommitHash,
updateMessage,
branchId,
codeSigningInfo,
runtimeVersions,
realizedPlatforms,
}: {
graphqlClient: ExpoGraphqlClient;
isGitWorkingTreeDirty: boolean | undefined;
gitCommitHash: string | undefined;
updateMessage: string;
branchId: string;
codeSigningInfo: CodeSigningInfo | undefined;
runtimeVersions: { platform: string; runtimeVersion: string }[];
realizedPlatforms: PublishPlatform[];
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
const rollbackInfoGroups = Object.fromEntries(
realizedPlatforms.map(platform => [platform, true])
);
// Sort the updates into different groups based on their platform specific runtime versions
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
({ runtimeVersion, platforms }) => {
const localRollbackInfoGroup = Object.fromEntries(
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
);
return {
branchId,
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
runtimeVersion,
message: updateMessage,
gitCommitHash,
isGitWorkingTreeDirty,
awaitingCodeSigningInfo: !!codeSigningInfo,
};
}
);
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
if (codeSigningInfo) {
Log.log('🔒 Signing roll back');
const updatesTemp = [...newUpdates];
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
const newUpdates = updatesTemp.splice(
0,
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
);
return {
updateGroup,
newUpdates,
};
});
await Promise.all(
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
await Promise.all(
newUpdates.map(async newUpdate => {
const response = await fetch(newUpdate.manifestPermalink, {
method: 'GET',
headers: { accept: 'multipart/mixed' },
});
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
const directiveSignature = signBody(directiveBody, codeSigningInfo);
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
alg: codeSigningInfo.codeSigningMetadata.alg,
keyid: codeSigningInfo.codeSigningMetadata.keyid,
sig: directiveSignature,
});
})
);
})
);
}
return newUpdates;
}
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
{ exit: 1 }
);
}
return {
auto,
branchName,
channelName,
updateMessage,
platform: flags.platform as RequestedPlatform,
privateKeyPath: flags['private-key-path'],
nonInteractive,
json: flags.json ?? false,
};
}
}
@@ -128,7 +128,7 @@ export default class IosCredentialsProvider {
private async disablePushNotificationsSetupInEasJsonAsync(
ctx: CredentialsContext
): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(ctx.projectDir);
const easJsonAccessor = new EasJsonAccessor(ctx.projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
easJsonRawObject.cli = {
@@ -8,7 +8,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
private easJsonAccessor: EasJsonAccessor;
constructor(projectDir: string, private platform: T) {
this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
this.easJsonAccessor = new EasJsonAccessor(projectDir);
}
async runAsync(): Promise<BuildProfile<T>> {
+110 -430
View File
File diff suppressed because it is too large. Load diff
@@ -1,66 +0,0 @@
import assert from 'assert';
import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../client';
import { BillingUpdateByAccountQuery } from '../generated';
export const BillingUpdateQuery = {
async byAccountIdAsync(
graphqlClient: ExpoGraphqlClient,
accountId: string,
date: Date
): Promise<BillingUpdateByAccountQuery['account']['byId']> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<BillingUpdateByAccountQuery>(
gql`
query BillingUpdateByAccountQuery($accountId: String!, $date: DateTime!) {
account {
byId(accountId: $accountId) {
id
billing {
id
subscription {
id
name
meteredBillingStatus {
EAS_UPDATE
}
}
}
usageMetrics {
byBillingPeriod(date: $date, service: UPDATES) {
id
planMetrics {
id
serviceMetric
metricType
value
limit
}
}
}
}
}
}
`,
{ accountId, date },
{
additionalTypenames: [
'Account',
'Billing',
'SubscriptionDetails',
'AccountUsageMetrics',
'UsageMetricTotal',
'EstimatedUsage',
],
}
)
.toPromise()
);
assert(data.account, 'GraphQL: `account` not defined in server response');
return data.account.byId;
},
};
@@ -1,29 +0,0 @@
import gql from 'graphql-tag';
export const AccountUpdatesUsageFragmentNode = gql`
fragment AccountUpdatesUsageFragment on Account {
id
billing {
id
subscription {
id
name
meteredBillingStatus {
EAS_UPDATE
}
}
}
usageMetrics {
byBillingPeriod(date: $date, service: UPDATES) {
id
planMetrics {
id
serviceMetric
metricType
value
limit
}
}
}
}
`;
@@ -9,7 +9,6 @@ export const UpdateFragmentNode = gql`
runtimeVersion
platform
manifestFragment
isRollBackToEmbedded
manifestPermalink
gitCommitHash
actor {
@@ -1,3 +1,4 @@
import { EasJsonUtils } from '@expo/eas-json';
import fs from 'fs-extra';
import mockdate from 'mockdate';
import path from 'path';
@@ -5,15 +6,16 @@ import { instance, mock } from 'ts-mockito';
import { v4 as uuidv4 } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { defaultPublishPlatforms } from '../../commands/update';
import { AssetMetadataStatus } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { PublishQuery } from '../../graphql/queries/PublishQuery';
import {
CollectedAssets,
MetadataJoi,
buildUnsortedUpdateInfoGroupAsync,
collectAssetsAsync,
convertAssetToUpdateInfoGroupFormatAsync,
defaultPublishPlatforms,
filterExportedPlatformsByFlag,
filterOutAssetsThatAlreadyExistAsync,
getAssetHashFromPath,
@@ -28,6 +30,17 @@ import {
jest.mock('../../uploads');
jest.mock('fs');
jest.mock('@expo/eas-json', () => {
const actual = jest.requireActual('@expo/eas-json');
const EasJsonUtilsMock = {
getUpdatesConfigAsync: jest.fn(),
};
return {
...actual,
EasJsonUtils: EasJsonUtilsMock,
};
});
const dummyFileBuffer = Buffer.from('dummy-file');
@@ -96,20 +109,38 @@ describe('MetadataJoi', () => {
describe(filterExportedPlatformsByFlag, () => {
it(`returns all`, () => {
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'all')).toEqual({
web: true,
ios: true,
android: true,
});
expect(
filterExportedPlatformsByFlag(
new Map([
['web', {}],
['ios', {}],
['android', {}],
]),
'all'
)
).toEqual(
new Map([
['web', {}],
['ios', {}],
['android', {}],
])
);
});
it(`selects a platform`, () => {
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'ios')).toEqual({
ios: true,
});
expect(
filterExportedPlatformsByFlag(
new Map([
['web', {}],
['ios', {}],
['android', {}],
]),
'ios'
)
).toEqual(new Map([['ios', {}]]));
});
it(`asserts selected platform missing`, () => {
expect(() =>
filterExportedPlatformsByFlag({ web: true }, 'ios')
filterExportedPlatformsByFlag(new Map([['web', {}]]), 'ios')
).toThrowErrorMatchingInlineSnapshot(
`"--platform="ios" not found in metadata.json. Available platform(s): web"`
);
@@ -207,22 +238,25 @@ describe(buildUnsortedUpdateInfoGroupAsync, () => {
it('returns the correct value', async () => {
await expect(
buildUnsortedUpdateInfoGroupAsync(
{
android: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'bundle/javascript',
path: androidBundlePath,
},
assets: [
{
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: assetPath,
new Map([
[
'android',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'bundle/javascript',
path: androidBundlePath,
},
],
},
},
assets: [
{
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: assetPath,
},
],
},
],
]),
{
slug: 'hello',
name: 'hello',
@@ -394,31 +428,155 @@ describe(collectAssetsAsync, () => {
})
);
expect(await collectAssetsAsync(inputDir)).toEqual({
android: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/android.js`,
},
assets: userDefinedAssets,
const projectDir = path.resolve();
expect(await collectAssetsAsync(projectDir, inputDir)).toEqual({
excludedAssets: [],
collectedAssets: new Map([
[
'android',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/android.js`,
},
assets: userDefinedAssets,
},
],
[
'ios',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/ios.js`,
},
assets: userDefinedAssets,
},
],
[
'web',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/web.js`,
},
assets: userDefinedAssets,
},
],
]),
});
});
it('supports asset exclusion', async () => {
const fakeHash = 'hdahukw8adhawi8fawhfa8';
const bundles = {
android: 'android-bundle-code',
ios: 'ios-bundle-code',
web: 'web-bundle-code',
};
const inputDir = path.resolve(uuidv4());
const userDefinedAssets = [
{
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: `${inputDir}/assets/${fakeHash}`,
originalPath: `assets/wat.jpg`,
},
ios: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/ios.js`,
];
const bundleDir = path.resolve(`${inputDir}/bundles`);
const assetDir = path.resolve(`${inputDir}/assets`);
await fs.mkdir(bundleDir, { recursive: true });
await fs.mkdir(assetDir, { recursive: true });
for (const platform of defaultPublishPlatforms) {
await fs.writeFile(path.resolve(inputDir, `bundles/${platform}.js`), bundles[platform]);
}
await fs.writeFile(path.resolve(`${inputDir}/assets/${fakeHash}`), dummyFileBuffer);
await fs.writeFile(
path.resolve(inputDir, 'metadata.json'),
JSON.stringify({
version: 0,
bundler: 'metro',
fileMetadata: {
android: {
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
bundle: 'bundles/android.js',
},
ios: {
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
bundle: 'bundles/ios.js',
},
web: {
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
bundle: 'bundles/web.js',
},
},
assets: userDefinedAssets,
},
web: {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/web.js`,
})
);
await fs.writeFile(
path.resolve(inputDir, 'assetmap.json'),
JSON.stringify({
[fakeHash]: {
__packager_asset: true,
fileSystemLocation: '/Users/blah/temp/assets',
httpServerLocation: 'assets/assets',
width: 2339,
height: 1560,
scales: [1],
files: ['/Users/blah/temp/assets/wat.jpg'],
hash: fakeHash,
name: 'wat',
type: 'jpg',
fileHashes: [fakeHash],
},
assets: userDefinedAssets,
},
})
);
jest.mocked(EasJsonUtils.getUpdatesConfigAsync).mockResolvedValue({
assetExcludePatterns: ['**/*.jpg'],
});
const projectDir = path.resolve();
expect(await collectAssetsAsync(projectDir, inputDir)).toEqual({
excludedAssets: userDefinedAssets,
collectedAssets: new Map([
[
'android',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/android.js`,
},
assets: [],
},
],
[
'ios',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/ios.js`,
},
assets: [],
},
],
[
'web',
{
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: `${inputDir}/bundles/web.js`,
},
assets: [],
},
],
]),
});
});
});
@@ -488,40 +646,46 @@ describe(uploadAssetsAsync, () => {
path: userDefinedPath,
};
const assetsForUpdateInfoGroup = {
android: {
launchAsset: {
type: 'bundle',
contentType: 'application/javascript',
path: androidBundlePath,
},
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
const assetsForUpdateInfoGroup: CollectedAssets = new Map([
[
'android',
{
launchAsset: {
type: 'bundle',
contentType: 'application/javascript',
path: androidBundlePath,
},
],
},
ios: {
launchAsset: {
type: 'bundle',
contentType: 'application/javascript',
path: androidBundlePath,
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
],
[
'ios',
{
launchAsset: {
type: 'bundle',
contentType: 'application/javascript',
path: androidBundlePath,
},
],
},
};
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
],
]);
beforeAll(async () => {
await fs.writeFile(androidBundlePath, publishBundles.android.code);
@@ -74,7 +74,7 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
const [moduleName, taskName] =
splitCmd.length > 1 ? [splitCmd[0], splitCmd[1]] : [undefined, splitCmd[0]];
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)(Release|Debug)/);
const matchResult = taskName.match(/(build|bundle|assemble)(.*)(Release|Debug)/);
if (!matchResult) {
throw new Error(`Failed to parse gradle command: ${cmd}`);
}
@@ -1,47 +0,0 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { errors, readAndValidateBuildConfigAsync } from '@expo/steps';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
export async function validateCustomBuildConfigAsync(
projectDir: string,
profile: BuildProfile<Platform>
): Promise<void> {
if (!profile.config) {
return;
}
const relativeConfigPath = getCustomBuildConfigPath(profile.config);
const configPath = path.join(projectDir, relativeConfigPath);
if (!(await fs.pathExists(configPath))) {
throw new Error(
`Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.`
);
}
try {
await readAndValidateBuildConfigAsync(configPath, { skipNamespacedFunctionsCheck: true });
} catch (err) {
if (err instanceof errors.BuildConfigYAMLError) {
throw new Error(
`Custom build configuration file ${chalk.bold(
relativeConfigPath
)} contains invalid YAML.\n\n${err.message}`
);
} else if (err instanceof errors.BuildConfigError) {
throw new Error(
`Custom build configuration file ${chalk.bold(
relativeConfigPath
)} contains invalid configuration. Please check the docs!\n${err.message}`
);
} else {
throw err;
}
}
}
export function getCustomBuildConfigPath(configFilename: string): string {
return path.join('.eas/build', configFilename);
}
+106 -252
View File
@@ -1,36 +1,28 @@
import { ExpoConfig, Platform } from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import assert from 'assert';
import chalk from 'chalk';
import crypto from 'crypto';
import fs from 'fs-extra';
import Joi from 'joi';
import mime from 'mime';
import minimatch from 'minimatch';
import nullthrows from 'nullthrows';
import path from 'path';
import promiseLimit from 'promise-limit';
import { selectBranchOnAppAsync } from '../branch/queries';
import { getDefaultBranchNameAsync } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
import { PublishMutation } from '../graphql/mutations/PublishMutation';
import { PublishQuery } from '../graphql/queries/PublishQuery';
import Log, { learnMore } from '../log';
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
import { promptAsync } from '../prompts';
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
import Log from '../log';
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
import chunk from '../utils/expodash/chunk';
import { truthy } from '../utils/expodash/filter';
import mapMap from '../utils/expodash/mapMap';
import mapMapAsync from '../utils/expodash/mapMapAsync';
import partition from '../utils/expodash/partition';
import uniqBy from '../utils/expodash/uniqBy';
import { getVcsClient } from '../vcs';
import { resolveWorkflowAsync } from './workflow';
export type ExpoCLIExportPlatformFlag = Platform | 'all';
@@ -49,12 +41,15 @@ export type RawAsset = {
originalPath?: string;
};
type CollectedAssets = {
[platform in Platform]?: {
export type CollectedAssets = Map<
Platform,
{
launchAsset: RawAsset;
assets: RawAsset[];
};
};
}
>;
type ExcludedAssets = RawAsset[];
type ManifestExtra = {
expoClient?: { [key: string]: any };
@@ -159,20 +154,21 @@ export async function buildUnsortedUpdateInfoGroupAsync(
assets: CollectedAssets,
exp: ExpoConfig
): Promise<UpdateInfoGroup> {
let platform: Platform;
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
for (platform in assets) {
updateInfoGroup[platform] = {
launchAsset: await convertAssetToUpdateInfoGroupFormatAsync(assets[platform]?.launchAsset!),
assets: await Promise.all(
(assets[platform]?.assets ?? []).map(convertAssetToUpdateInfoGroupFormatAsync)
),
extra: {
expoClient: exp,
},
};
}
return updateInfoGroup as UpdateInfoGroup;
return Object.fromEntries(
(
await mapMapAsync(assets, async (collectedAssets): Promise<ManifestFragment> => {
return {
launchAsset: await convertAssetToUpdateInfoGroupFormatAsync(collectedAssets.launchAsset),
assets: await Promise.all(
collectedAssets.assets.map(convertAssetToUpdateInfoGroupFormatAsync)
),
extra: {
expoClient: exp,
},
};
})
).entries()
) as UpdateInfoGroup;
}
export async function buildBundlesAsync({
@@ -254,25 +250,25 @@ export function loadMetadata(distRoot: string): Metadata {
return metadata;
}
export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform, any>>>(
record: T,
export function filterExportedPlatformsByFlag<V>(
record: Map<Platform, V>,
platformFlag: ExpoCLIExportPlatformFlag
): T {
): Map<Platform, V> {
if (platformFlag === 'all') {
return record;
}
const platform = platformFlag as Platform;
if (!record[platform]) {
if (!record.has(platform)) {
throw new Error(
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Object.keys(
record
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Array.from(
record.keys()
).join(', ')}`
);
}
return { [platform]: record[platform] } as T;
return new Map([[platform, nullthrows(record.get(platform))]]);
}
/** Try to load the asset map for logging the names of assets published */
@@ -314,29 +310,77 @@ export function getOriginalPathFromAssetMap(
}
/** Given a directory, load the metadata.json and collect the assets for each platform. */
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
const metadata = loadMetadata(dir);
const assetmap = await loadAssetMapAsync(dir);
export async function collectAssetsAsync(
projectDir: string,
distRoot: string
): Promise<{ collectedAssets: CollectedAssets; excludedAssets: ExcludedAssets }> {
const metadata = loadMetadata(distRoot);
const assetmap = await loadAssetMapAsync(distRoot);
const collectedAssets: CollectedAssets = {};
const easJsonAccessor = new EasJsonAccessor(projectDir);
const updatesConfig = await EasJsonUtils.getUpdatesConfigAsync(easJsonAccessor);
const assetExcludePatterns = updatesConfig?.assetExcludePatterns;
for (const platform of Object.keys(metadata.fileMetadata) as Platform[]) {
collectedAssets[platform] = {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
},
assets: metadata.fileMetadata[platform].assets.map(asset => ({
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(dir, asset.path),
})),
const platformToAssets = new Map(
(Object.keys(metadata.fileMetadata) as Platform[]).map(platform => [
platform,
metadata.fileMetadata[platform].assets,
])
);
const platformToIncludedAndExcludedAssets = mapMap(platformToAssets, assets => {
if (!assetExcludePatterns) {
return {
includedAssets: assets,
excludedAssets: [],
};
}
const [includedAssets, excludedAssets] = partition(assets, asset => {
const originalPath = getOriginalPathFromAssetMap(assetmap, asset) ?? undefined;
return (
!originalPath || !assetExcludePatterns.some(pattern => minimatch(originalPath, pattern))
);
});
return {
includedAssets,
excludedAssets,
};
}
});
return collectedAssets;
const transformAsset = (asset: { path: string; ext: string }): RawAsset => {
return {
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
contentType: guessContentTypeFromExtension(asset.ext),
path: path.join(distRoot, asset.path),
};
};
const collectedAssets = mapMap(
platformToIncludedAndExcludedAssets,
({ includedAssets }, platform) => {
return {
launchAsset: {
fileExtension: '.bundle',
contentType: 'application/javascript',
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
},
assets: includedAssets.map(transformAsset),
};
}
);
const excludedAssets = uniqBy(
Array.from(
mapMap(platformToIncludedAndExcludedAssets, ({ excludedAssets }) =>
excludedAssets.map(transformAsset)
).values()
).flat(),
rawAsset => rawAsset.path
);
return { collectedAssets, excludedAssets };
}
// ensure the file extension has a '.' prefix
@@ -384,16 +428,11 @@ export async function uploadAssetsAsync(
updateSpinnerText?: (totalAssets: number, missingAssets: number) => void
): Promise<AssetUploadResult> {
let assets: RawAsset[] = [];
let platform: keyof CollectedAssets;
const launchAssets: RawAsset[] = [];
for (platform in assetsForUpdateInfoGroup) {
launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset);
assets = [
...assets,
assetsForUpdateInfoGroup[platform]!.launchAsset,
...assetsForUpdateInfoGroup[platform]!.assets,
];
}
mapMap(assetsForUpdateInfoGroup, collectedAssets => {
launchAssets.push(collectedAssets.launchAsset);
assets = [...assets, collectedAssets.launchAsset, ...collectedAssets.assets];
});
const assetsWithStorageKey = await Promise.all(
assets.map(async asset => {
@@ -467,188 +506,3 @@ export function isUploadedAssetCountAboveWarningThreshold(
const warningThreshold = Math.floor(assetLimitPerUpdateGroup * 0.75);
return uploadedAssetCount > warningThreshold;
}
export async function getBranchNameForCommandAsync({
graphqlClient,
projectId,
channelNameArg,
branchNameArg,
autoFlag,
nonInteractive,
paginatedQueryOptions,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
channelNameArg: string | undefined;
branchNameArg: string | undefined;
autoFlag: boolean;
nonInteractive: boolean;
paginatedQueryOptions: PaginatedQueryOptions;
}): Promise<string> {
if (channelNameArg && branchNameArg) {
throw new Error(
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
);
}
if (channelNameArg) {
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
}
if (branchNameArg) {
return branchNameArg;
}
if (autoFlag) {
return await getDefaultBranchNameAsync();
} else if (nonInteractive) {
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode.');
} else {
let branchName: string;
try {
const branch = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like to use?`,
displayTextForListItem: updateBranch => ({
title: `${updateBranch.name} ${chalk.grey(
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
)}`,
}),
paginatedQueryOptions,
});
branchName = branch.name;
} catch {
// unable to select a branch (network error or no branches for project)
const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'No branches found. Provide a branch name:',
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : 'Branch name may not be empty.'),
});
branchName = name;
}
assert(branchName, 'Branch name must be specified.');
return branchName;
}
}
export async function getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
jsonFlag,
}: {
updateMessageArg: string | undefined;
autoFlag: boolean;
nonInteractive: boolean;
jsonFlag: boolean;
}): Promise<string> {
let updateMessage = updateMessageArg;
if (!updateMessageArg && autoFlag) {
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
}
if (!updateMessage) {
if (nonInteractive) {
throw new Error('Must supply --message or use --auto when in non-interactive mode');
}
const validationMessage = 'publish message may not be empty.';
if (jsonFlag) {
throw new Error(validationMessage);
}
const { updateMessageLocal } = await promptAsync({
type: 'text',
name: 'updateMessageLocal',
message: `Provide an update message:`,
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
validate: (value: any) => (value ? true : validationMessage),
});
updateMessage = updateMessageLocal;
}
assert(updateMessage, 'Update message must be specified.');
const truncatedMessage = truncateUpdateMessage(updateMessage, 1024);
if (truncatedMessage !== updateMessage) {
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
}
return updateMessage;
}
export const defaultPublishPlatforms: Platform[] = ['android', 'ios'];
export function getRequestedPlatform(
platform: ExpoCLIExportPlatformFlag
): RequestedPlatform | null {
switch (platform) {
case 'android':
return RequestedPlatform.Android;
case 'ios':
return RequestedPlatform.Ios;
case 'web':
return null;
case 'all':
return RequestedPlatform.All;
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
export async function getRuntimeVersionObjectAsync(
exp: ExpoConfig,
platforms: Platform[],
projectDir: string
): Promise<{ platform: string; runtimeVersion: string }[]> {
for (const platform of platforms) {
if (platform === 'web') {
continue;
}
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
if (isPolicy) {
const isManaged =
(await resolveWorkflowAsync(projectDir, platform as EASBuildJobPlatform)) ===
Workflow.MANAGED;
if (!isManaged) {
throw new Error(
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
);
}
}
}
return [...new Set(platforms)].map(platform => {
if (platform === 'web') {
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
}
return {
platform,
runtimeVersion: nullthrows(
Updates.getRuntimeVersion(exp, platform),
`Unable to determine runtime version for ${
requestedPlatformDisplayNames[platform]
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
),
};
});
}
export function getRuntimeToPlatformMappingFromRuntimeVersions(
runtimeVersions: { platform: string; runtimeVersion: string }[]
): { runtimeVersion: string; platforms: string[] }[] {
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
for (const runtime of runtimeVersions) {
const platforms = runtimeVersions
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
.map(({ platform }) => platform);
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
}
}
return runtimeToPlatformMapping;
}
@@ -124,7 +124,6 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
runtime: {} as Runtime, // Temporary fix to resolve type errors
platform: 'ios',
manifestFragment: '...',
isRollBackToEmbedded: false,
activityTimestamp: '2022-12-07T02:24:43.487Z',
branchId: '',
awaitingCodeSigningInfo: false,
+2 -2
View File
@@ -273,7 +273,7 @@ export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: stri
}
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonAccessor = new EasJsonAccessor(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
@@ -343,7 +343,7 @@ export async function ensureEASUpdateIsConfiguredAsync(
expWithoutUpdates.sdkVersion
);
if (!hasExpoUpdates) {
await installExpoUpdatesAsync(projectDir, { silent: false });
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
Log.withTick('Installed expo updates');
}
-11
View File
@@ -48,8 +48,6 @@ export type FormattedUpdateGroupDescription = {
group: string;
platforms: string;
runtimeVersion: string;
codeSigningKey: string | undefined;
isRollBackToEmbedded: boolean;
};
export type FormattedBranchDescription = {
@@ -76,8 +74,6 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri
{ label: 'Platforms', value: update.platforms },
{ label: 'Runtime Version', value: update.runtimeVersion },
{ label: 'Message', value: update.message },
{ label: 'Code Signing Key', value: update.codeSigningKey ?? 'N/A' },
{ label: 'Is Roll Back to Embedded', value: update.isRollBackToEmbedded ? 'Yes' : 'No' },
{ label: 'Group ID', value: update.group },
]);
}
@@ -207,7 +203,6 @@ export function getUpdateGroupJsonInfo(updateGroups: UpdateFragment[]): UpdateJs
runtimeVersion: update.runtimeVersion,
platform: update.platform,
manifestPermalink: update.manifestPermalink,
isRollBackToEmbedded: update.isRollBackToEmbedded,
gitCommitHash: update.gitCommitHash,
}));
}
@@ -218,8 +213,6 @@ export function getUpdateGroupDescriptions(
return updateGroups.map(updateGroup => ({
message: formatUpdateMessage(updateGroup[0]),
runtimeVersion: updateGroup[0].runtimeVersion,
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
group: updateGroup[0].group,
platforms: formatPlatformForUpdateGroup(updateGroup),
}));
@@ -232,8 +225,6 @@ export function getUpdateGroupDescriptionsWithBranch(
branch: updateGroup[0].branch.name,
message: formatUpdateMessage(updateGroup[0]),
runtimeVersion: updateGroup[0].runtimeVersion,
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
group: updateGroup[0].group,
platforms: formatPlatformForUpdateGroup(updateGroup),
}));
@@ -250,8 +241,6 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
update: {
message: formatUpdateMessage(latestUpdate),
runtimeVersion: latestUpdate.runtimeVersion,
isRollBackToEmbedded: latestUpdate.isRollBackToEmbedded,
codeSigningKey: latestUpdate.codeSigningInfo?.keyid,
group: latestUpdate.group,
platforms: getPlatformsForGroup({
group: latestUpdate.group,
@@ -11,7 +11,6 @@ jest.mock('@expo/eas-json', () => {
const EasJsonUtilsMock = {
getBuildProfileAsync: jest.fn(),
getBuildProfileNamesAsync: jest.fn(),
getBuildProfileDeprecationWarnings: jest.fn(() => []),
};
return {
...actual,
@@ -30,7 +29,7 @@ describe(getProfilesAsync, () => {
});
it('defaults to production profile', async () => {
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
const easJsonAccessor = new EasJsonAccessor('/fake');
const result = await getProfilesAsync({
easJsonAccessor,
platforms: [Platform.ANDROID, Platform.IOS],
@@ -52,7 +51,7 @@ describe(getProfilesAsync, () => {
await expect(
getProfilesAsync({
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
easJsonAccessor: new EasJsonAccessor('/fake'),
platforms: [Platform.ANDROID],
profileName: undefined,
type: 'build',
@@ -61,7 +60,7 @@ describe(getProfilesAsync, () => {
});
it('gets a specific profile', async () => {
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
const easJsonAccessor = new EasJsonAccessor('/fake');
const result = await getProfilesAsync({
easJsonAccessor,
platforms: [Platform.ANDROID, Platform.IOS],
@@ -86,7 +85,7 @@ describe(getProfilesAsync, () => {
await expect(
getProfilesAsync({
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
easJsonAccessor: new EasJsonAccessor('/fake'),
platforms: [Platform.ANDROID, Platform.IOS],
profileName: undefined,
type: 'build',
-44
View File
@@ -1,44 +0,0 @@
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { BillingUpdateQuery } from '../graphql/queries/BillingUpdateQuery';
import Log from '../log';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
const WARNING_THRESHOLD = 0.85;
export async function maybeWarnAboutEasPlanOverageAsync(
graphqlClient: ExpoGraphqlClient,
projectId: string
): Promise<void> {
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const billingUpdateQuery = await BillingUpdateQuery.byAccountIdAsync(
graphqlClient,
account.id,
new Date()
);
const isFree = !billingUpdateQuery.billing; // TODO: no billing means free?
const isMetered = !!billingUpdateQuery.billing?.subscription?.meteredBillingStatus?.EAS_UPDATE;
// TODO: verify correctness
if (!isFree || isMetered) {
return;
}
const planMetrics = billingUpdateQuery.usageMetrics.byBillingPeriod.planMetrics;
for (const metric of planMetrics) {
const { value, limit, serviceMetric } = metric;
const percentageUsed = value / limit;
if (percentageUsed >= 1) {
Log.error(
'You have exceeded your EAS Update plan limit. You can create updates but they will no longer be distributed to users.'
);
Log.error('Please upgrade your plan to continue receiving updates.');
return;
} else if (percentageUsed >= WARNING_THRESHOLD && percentageUsed < 1) {
Log.warn(
`You are approaching your EAS Update plan limit. You have used ${Math.round(
percentageUsed * 100
)}% of your plan's ${serviceMetric}.`
);
Log.warn('Please upgrade your plan to continue receiving updates.');
}
}
}
+4 -30
View File
@@ -16,9 +16,8 @@ import nullthrows from 'nullthrows';
import { Response } from '../fetch';
import { PartialManifest, PartialManifestAsset } from '../graphql/generated';
import areSetsEqual from './expodash/areSetsEqual';
export type CodeSigningInfo = {
type CodeSigningInfo = {
privateKey: PKI.rsa.PrivateKey;
certificate: PKI.Certificate;
codeSigningMetadata: { alg: string; keyid: string };
@@ -102,7 +101,7 @@ export async function getKeyAndCertificateFromPathsAsync({
};
}
async function getMultipartBodyPartAsync(res: Response, partName: string): Promise<string | null> {
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
const contentType = res.headers.get('content-type');
if (!contentType) {
throw new Error('The multipart manifest response is missing the content-type header');
@@ -112,19 +111,11 @@ async function getMultipartBodyPartAsync(res: Response, partName: string): Promi
contentType,
Buffer.from(bodyBuffer)
);
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, partName));
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, 'manifest'));
return manifestPart?.body ?? null;
}
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
return await getMultipartBodyPartAsync(res, 'manifest');
}
export async function getDirectiveBodyAsync(res: Response): Promise<string | null> {
return await getMultipartBodyPartAsync(res, 'directive');
}
export function signBody(body: string, codeSigningInfo: CodeSigningInfo): string {
export function signManifestBody(body: string, codeSigningInfo: CodeSigningInfo): string {
return signBufferRSASHA256AndVerify(
codeSigningInfo.privateKey,
codeSigningInfo.certificate,
@@ -190,20 +181,3 @@ export function checkManifestBodyAgainstUpdateInfoGroup(
assertAssetParity(correspondingManifestResponseBodyAssetJSON, nullthrows(partialManifestAsset));
}
}
export function checkDirectiveBodyAgainstUpdateInfoGroup(directiveResponseBody: string): void {
const directiveResponseBodyJSON = JSON.parse(directiveResponseBody);
if (
!areSetsEqual(
new Set(Object.keys(directiveResponseBodyJSON)),
new Set(['extra', 'type', 'parameters'])
)
) {
throw new Error('Code signing directive integrity error: Unexpected keys');
}
if (directiveResponseBodyJSON.type !== 'rollBackToEmbedded') {
throw new Error('Code signing directive integrity error: Incorrect directive type');
}
}
+2 -3
View File
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config-types';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { boolish } from 'getenv';
import resolveFrom, { silent as silentResolveFrom } from 'resolve-from';
import resolveFrom from 'resolve-from';
import semver from 'semver';
import Log from '../log';
@@ -63,8 +63,7 @@ export async function expoCommandAsync(
args: string[],
{ silent = false }: { silent?: boolean } = {}
): Promise<void> {
const expoCliPath =
silentResolveFrom(projectDir, 'expo/bin/cli') ?? resolveFrom(projectDir, 'expo/bin/cli.js');
const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js');
const spawnPromise = spawnAsync(expoCliPath, args, {
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command
});
@@ -1,17 +0,0 @@
import areSetsEqual from '../areSetsEqual';
describe(areSetsEqual, () => {
it.each([
[new Set([1, 2]), new Set([1, 2])],
[new Set([1, 2]), new Set([2, 1])],
])('equal cases: %p', (a, b) => {
expect(areSetsEqual(a, b)).toBe(true);
});
it.each([
[new Set([1, 2, 3]), new Set([1, 2])],
[new Set([1, 2]), new Set([1, 2, 3])],
])('non-equal cases: %p', (a, b) => {
expect(areSetsEqual(a, b)).toBe(false);
});
});
@@ -0,0 +1,13 @@
import { truthy } from '../filter';
import partition from '../partition';
describe(partition, () => {
it('partitions', () => {
expect(partition([true, false], truthy)).toMatchObject([[true], [false]]);
expect(partition([1, 2, 3, 4], e => e <= 2)).toMatchObject([
[1, 2],
[3, 4],
]);
});
});
@@ -1,3 +0,0 @@
export default function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
return a.size === b.size && [...a].every(value => b.has(value));
}
@@ -0,0 +1,10 @@
export default function mapMap<K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => M
): Map<K, M> {
const resultingMap = new Map();
for (const [k, v] of map) {
resultingMap.set(k, mapper(v, k));
}
return resultingMap;
}
@@ -0,0 +1,14 @@
export default async function mapMapAsync<K, V, M>(
map: ReadonlyMap<K, V>,
mapper: (value: V, key: K) => Promise<M>
): Promise<Map<K, M>> {
const resultingMap: Map<K, M> = new Map();
await Promise.all(
Array.from(map.keys()).map(async k => {
const initialValue = map.get(k) as V;
const result = await mapper(initialValue, k);
resultingMap.set(k, result);
})
);
return resultingMap;
}
@@ -0,0 +1,14 @@
export default function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]] {
const trueItems: T[] = [];
const falseItems: T[] = [];
for (const item of arr) {
if (predicate(item)) {
trueItems.push(item);
} else {
falseItems.push(item);
}
}
return [trueItems, falseItems];
}
+2 -37
View File
@@ -7,8 +7,6 @@ import {
SubmitProfile,
} from '@expo/eas-json';
import Log, { learnMore } from '../log';
type EasProfile<T extends ProfileType> = T extends 'build'
? BuildProfile<Platform>
: SubmitProfile<Platform>;
@@ -59,15 +57,11 @@ async function readProfileAsync<T extends ProfileType>({
profileName?: string;
}): Promise<EasProfile<T>> {
if (type === 'build') {
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
return (await EasJsonUtils.getBuildProfileAsync(
easJsonAccessor,
platform,
profileName
);
maybePrintBuildProfileDeprecationWarnings(buildProfile, profileName);
return buildProfile as EasProfile<T>;
)) as EasProfile<T>;
} else {
return (await EasJsonUtils.getSubmitProfileAsync(
easJsonAccessor,
@@ -76,32 +70,3 @@ async function readProfileAsync<T extends ProfileType>({
)) as EasProfile<T>;
}
}
let hasPrintedDeprecationWarnings = false;
function maybePrintBuildProfileDeprecationWarnings(
buildProfile: BuildProfile<Platform>,
profileName?: string
): void {
if (hasPrintedDeprecationWarnings) {
return;
}
const deprecationWarnings = EasJsonUtils.getBuildProfileDeprecationWarnings(
buildProfile,
profileName
);
if (deprecationWarnings.length === 0) {
return;
}
Log.newLine();
Log.warn('Detected deprecated fields in eas.json:');
for (const warning of deprecationWarnings) {
const warnlog: string = warning.message.map(line => `\t${line}`).join('\n');
Log.warn(warnlog);
if (warning.docsUrl) {
Log.warn(`\t${learnMore(warning.docsUrl)}`);
}
Log.newLine();
}
hasPrintedDeprecationWarnings = true;
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "3.9.2",
"version": "3.6.1",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "1.0.7",
"@expo/eas-build-job": "0.2.102",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
+6 -1
View File
@@ -74,6 +74,11 @@
"items": {
"type": "string"
}
},
"cacheDefaultPaths": {
"type": "boolean",
"description": "Specifies whether to cache the recommended set of files, currently only Podfile.lock is cached by default for iOS build and nothing is cached for Android.",
"default": true
}
}
},
@@ -349,7 +354,7 @@
"default": "default",
"anyOf": [
{
"enum": ["default", "medium", "intel-medium", "m-medium"]
"enum": ["default", "medium", "m1-medium", "intel-medium"]
},
{
"type": "string"
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`fromProjectPath patching JSON file 1`] = `
exports[`EasJsonAccessor patching JSON file 1`] = `
"{
"build": {
"production": {
@@ -14,7 +14,7 @@ exports[`fromProjectPath patching JSON file 1`] = `
"
`;
exports[`fromProjectPath patching JSON5 file (preserves comments) 1`] = `
exports[`EasJsonAccessor patching JSON5 file (preserves comments) 1`] = `
"{
"build": {
"production": {
@@ -15,7 +15,7 @@ beforeEach(async () => {
vol.reset();
});
describe(EasJsonAccessor.fromProjectPath, () => {
describe(EasJsonAccessor, () => {
test('patching JSON file', async () => {
vol.fromJSON(
{
@@ -24,7 +24,7 @@ describe(EasJsonAccessor.fromProjectPath, () => {
fakeAppPath
);
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
const accessor = new EasJsonAccessor(fakeAppPath);
await accessor.readAsync();
accessor.patch(o => {
o.build.production.env.ABC = '456';
@@ -44,7 +44,7 @@ describe(EasJsonAccessor.fromProjectPath, () => {
fakeAppPath
);
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
const accessor = new EasJsonAccessor(fakeAppPath);
await accessor.readAsync();
accessor.patch(o => {
o.build.production.env.ABC = '456';
@@ -67,7 +67,7 @@ describe(EasJsonAccessor.fromProjectPath, () => {
fakeAppPath
);
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
const accessor = new EasJsonAccessor(fakeAppPath);
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
await expect(accessor.readAsync()).rejects.toThrowError(
/^Found invalid character in .*eas\.json.+/
@@ -82,57 +82,7 @@ describe(EasJsonAccessor.fromProjectPath, () => {
fakeAppPath
);
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
});
});
describe(EasJsonAccessor.fromRawString, () => {
test('patching JSON file', async () => {
const accessor = EasJsonAccessor.fromRawString(
await fsReal.readFile(path.join(fixturesDir, 'eas-json.json'), 'utf-8')
);
await accessor.readAsync();
expect(() =>
accessor.patch(o => {
o.build.production.env.ABC = '456';
return o;
})
).toThrowError();
});
test('reading invalid JSON5 object', async () => {
vol.fromJSON(
{
'./eas.json': await fsReal.readFile(
path.join(fixturesDir, 'eas-invalid-json5.json'),
'utf-8'
),
},
fakeAppPath
);
const accessor = EasJsonAccessor.fromRawString(
await fsReal.readFile(path.join(fixturesDir, 'eas-invalid-json5.json'), 'utf-8')
);
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
await expect(accessor.readAsync()).rejects.toThrowError(
/^Found invalid character in .*eas\.json.+/
);
});
test('reading empty JSON file', async () => {
vol.fromJSON(
{
'./eas.json': await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8'),
},
fakeAppPath
);
const accessor = EasJsonAccessor.fromRawString(
await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8')
);
const accessor = new EasJsonAccessor(fakeAppPath);
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
});
@@ -20,33 +20,7 @@ test('minimal valid eas.json for both platforms', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
expect(androidProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
});
expect(iosProfile).toEqual({
distribution: 'store',
credentialsSource: 'remote',
});
});
test('minimal valid eas.json for both platforms when reading eas.json from string', async () => {
const accessor = EasJsonAccessor.fromRawString(
JSON.stringify({
build: {
production: {},
},
})
);
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -78,7 +52,7 @@ test('valid eas.json for development client builds', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -108,7 +82,7 @@ test(`valid eas.json with autoIncrement flag at build profile root`, async () =>
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -137,7 +111,7 @@ test('valid profile for internal distribution on Android', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'internal');
expect(profile).toEqual({
distribution: 'internal',
@@ -159,7 +133,7 @@ test('valid profile extending other profile', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -202,7 +176,7 @@ test('valid profile extending other profile with platform specific envs', async
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -256,6 +230,7 @@ test('valid profile extending other profile with platform specific caching', asy
},
android: {
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
},
@@ -263,7 +238,7 @@ test('valid profile extending other profile with platform specific caching', asy
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
@@ -286,6 +261,7 @@ test('valid profile extending other profile with platform specific caching', asy
distribution: 'internal',
credentialsSource: 'remote',
cache: {
cacheDefaultPaths: false,
customPaths: ['somefakepath'],
},
});
@@ -306,7 +282,7 @@ test('valid eas.json with missing profile', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'debug');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
});
@@ -318,7 +294,7 @@ test('invalid eas.json when using wrong buildType', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
await expect(promise).rejects.toThrowError(
@@ -329,7 +305,7 @@ test('invalid eas.json when using wrong buildType', async () => {
test('empty json', async () => {
await fs.writeJson('/project/eas.json', {});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
});
@@ -341,7 +317,7 @@ test('invalid semver value', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
await expect(promise).rejects.toThrowError(
@@ -356,7 +332,7 @@ test('invalid release channel', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
});
@@ -369,7 +345,7 @@ test('get profile names', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const allProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(accessor);
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
});
@@ -383,7 +359,7 @@ test('invalid resourceClass at build profile root', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
await expect(
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
@@ -401,7 +377,7 @@ test('iOS-specific resourceClass', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
await expect(
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
).resolves.not.toThrow();
@@ -418,69 +394,8 @@ test('Android-specific resourceClass', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
await expect(
EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production')
).resolves.not.toThrow();
});
test('build profile with platform-specific custom build config', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: {
android: {
config: 'production.android.yml',
},
ios: {
config: 'production.ios.yml',
},
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
expect(androidProfile).toEqual({
config: 'production.android.yml',
distribution: 'store',
credentialsSource: 'remote',
});
expect(iosProfile).toEqual({
config: 'production.ios.yml',
distribution: 'store',
credentialsSource: 'remote',
});
});
test('build profiles with both platform build config', async () => {
await fs.writeJson('/project/eas.json', {
build: {
production: {
config: 'production.yml',
},
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
accessor,
Platform.ANDROID,
'production'
);
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
expect(androidProfile).toEqual({
config: 'production.yml',
distribution: 'store',
credentialsSource: 'remote',
});
expect(iosProfile).toEqual({
config: 'production.yml',
distribution: 'store',
credentialsSource: 'remote',
});
});
@@ -19,7 +19,7 @@ test('minimal allowed eas.json for both platforms', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
@@ -50,7 +50,7 @@ test('android config with all required values', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.ANDROID,
@@ -80,7 +80,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
try {
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
Platform.ANDROID,
@@ -114,7 +114,7 @@ test('ios config with all required values', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
expect(iosProfile).toEqual({
@@ -148,7 +148,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
process.env.ASC_API_KEY_ID = 'ABCD';
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
expect(iosProfile).toEqual({
@@ -189,7 +189,7 @@ test('valid profile extending other profile', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const baseProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'base');
const extendedProfile = await EasJsonUtils.getSubmitProfileAsync(
accessor,
@@ -233,7 +233,7 @@ test('get profile names', async () => {
},
});
const accessor = EasJsonAccessor.fromProjectPath('/project');
const accessor = new EasJsonAccessor('/project');
const allProfileNames = await EasJsonUtils.getSubmitProfileNamesAsync(accessor);
expect(allProfileNames.sort()).toEqual(['production', 'blah'].sort());
});
+11 -42
View File
@@ -38,7 +38,7 @@ const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
];
export class EasJsonAccessor {
private easJsonPath: string | undefined;
private easJsonPath: string;
private isJson5 = false;
private easJson: EasJson | undefined;
@@ -46,25 +46,8 @@ export class EasJsonAccessor {
private easJsonRawObject: any | undefined;
private easJsonPatched: boolean = false;
private constructor({ projectDir }: { projectDir: string });
private constructor({ easJsonRawContents }: { easJsonRawContents: string });
private constructor({
projectDir,
easJsonRawContents,
}: {
projectDir?: string;
easJsonRawContents?: string;
}) {
this.easJsonPath = projectDir && EasJsonAccessor.formatEasJsonPath(projectDir);
this.easJsonRawContents = easJsonRawContents;
}
public static fromProjectPath(projectDir: string): EasJsonAccessor {
return new EasJsonAccessor({ projectDir });
}
public static fromRawString(easJsonRawContents: string): EasJsonAccessor {
return new EasJsonAccessor({ easJsonRawContents });
constructor(projectDir: string) {
this.easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
}
public static formatEasJsonPath(projectDir: string): string {
@@ -98,9 +81,6 @@ export class EasJsonAccessor {
}
public async writeAsync(): Promise<void> {
if (!this.easJsonPath) {
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
}
if (!this.easJsonPatched) {
return;
}
@@ -109,9 +89,6 @@ export class EasJsonAccessor {
}
public patch(fn: (easJsonRawObject: any) => any): void {
if (!this.easJsonPath) {
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
}
assert(
this.easJsonRawContents && this.easJsonRawObject,
'call readAsync/readRawJsonAsync first'
@@ -127,24 +104,16 @@ export class EasJsonAccessor {
}
public async readRawJsonAsync(): Promise<any> {
if (this.easJsonPath) {
if (!(await fs.pathExists(this.easJsonPath))) {
throw new MissingEasJsonError(
`${chalk.bold('eas.json')} could not be found at ${
this.easJsonPath
}. Learn more at https://expo.fyi/eas-json`
);
}
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
if (!(await fs.pathExists(this.easJsonPath))) {
throw new MissingEasJsonError(
`${chalk.bold('eas.json')} could not be found at ${
this.easJsonPath
}. Learn more at https://expo.fyi/eas-json`
);
}
return this.parseRawJson();
}
private parseRawJson(): any {
assert(
this.easJsonRawContents !== undefined,
'easJsonRawContents needs to be set before calling parseRawJson'
);
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
if (this.easJsonRawContents.trim().length === 0) {
throw new InvalidEasJsonError(`${chalk.bold('eas.json')} is empty.`);
}
+41 -72
View File
@@ -14,8 +14,6 @@ const AllowedIosResourceClasses: ResourceClass[] = [
...AllowedCommonResourceClasses,
ResourceClass.M1_MEDIUM,
ResourceClass.INTEL_MEDIUM,
ResourceClass.M_MEDIUM,
ResourceClass.M_LARGE,
];
const CacheSchema = Joi.object({
@@ -26,98 +24,69 @@ const CacheSchema = Joi.object({
});
const CommonBuildProfileSchema = Joi.object({
// builder
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
distribution: Joi.string().valid('store', 'internal').default('store'),
cache: CacheSchema,
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
developmentClient: Joi.boolean(),
prebuildCommand: Joi.string(),
buildArtifactPaths: Joi.array().items(Joi.string()),
// build environment
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
node: Joi.string().empty(null).custom(semverCheck),
yarn: Joi.string().empty(null).custom(semverCheck),
expoCli: Joi.string().empty(null).custom(semverCheck),
// credentials
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
distribution: Joi.string().valid('store', 'internal').default('store'),
// updates
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
// build configuration
developmentClient: Joi.boolean(),
prebuildCommand: Joi.string(),
// versions
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
autoIncrement: Joi.alternatives().try(Joi.boolean()),
// artifacts
buildArtifactPaths: Joi.array().items(Joi.string()),
// cache
cache: CacheSchema,
// custom build configuration
config: Joi.string(),
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
});
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
// build environment
image: Joi.string(),
// artifacts
artifactPath: Joi.string(),
applicationArchivePath: Joi.string(),
}).oxor('artifactPath', 'applicationArchivePath')
);
const AndroidBuildProfileSchema = PlatformBuildProfileSchema.concat(
Joi.object({
// builder
resourceClass: Joi.string().valid(...AllowedAndroidResourceClasses),
// build environment
ndk: Joi.string().empty(null).custom(semverCheck),
// credentials
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
withoutCredentials: Joi.boolean(),
// build configuration
gradleCommand: Joi.string(),
buildType: Joi.string().valid('apk', 'app-bundle'),
// versions
image: Joi.string(),
ndk: Joi.string().empty(null).custom(semverCheck),
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'versionCode')
),
})
resourceClass: Joi.string().valid(...AllowedAndroidResourceClasses),
artifactPath: Joi.string(),
applicationArchivePath: Joi.string(),
buildArtifactPaths: Joi.array().items(Joi.string()),
gradleCommand: Joi.string(),
buildType: Joi.string().valid('apk', 'app-bundle'),
}).oxor('artifactPath', 'applicationArchivePath')
);
const IosBuildProfileSchema = PlatformBuildProfileSchema.concat(
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
// builder
resourceClass: Joi.string().valid(...AllowedIosResourceClasses),
// build environment
bundler: Joi.string().empty(null).custom(semverCheck),
fastlane: Joi.string().empty(null).custom(semverCheck),
cocoapods: Joi.string().empty(null).custom(semverCheck),
// credentials
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
// build configuration
simulator: Joi.boolean(),
scheme: Joi.string(),
buildConfiguration: Joi.string(),
// versions
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'buildNumber')
),
})
simulator: Joi.boolean(),
resourceClass: Joi.string().valid(...AllowedIosResourceClasses),
image: Joi.string(),
bundler: Joi.string().empty(null).custom(semverCheck),
fastlane: Joi.string().empty(null).custom(semverCheck),
cocoapods: Joi.string().empty(null).custom(semverCheck),
artifactPath: Joi.string(),
applicationArchivePath: Joi.string(),
buildArtifactPaths: Joi.array().items(Joi.string()),
scheme: Joi.string(),
buildConfiguration: Joi.string(),
}).oxor('artifactPath', 'applicationArchivePath')
);
export const BuildProfileSchema = CommonBuildProfileSchema.concat(
+31 -64
View File
@@ -9,21 +9,15 @@ export enum ResourceClass {
DEFAULT = 'default',
LARGE = 'large',
/**
* @deprecated use M_MEDIUM instead
* @deprecated use M1_MEDIUM instead
* @experimental
* This resource class is not yet ready to be used in production. For testing purposes only. Might be deprecated / deleted at any time.
*/
M1_EXPERIMENTAL = 'm1-experimental',
/**
* @deprecated use M_MEDIUM instead
*/
M1_MEDIUM = 'm1-medium',
/**
* @deprecated use M_LARGE instead
*/
M1_LARGE = 'm1-large',
INTEL_MEDIUM = 'intel-medium',
MEDIUM = 'medium',
M_MEDIUM = 'm-medium',
M_LARGE = 'm-large',
}
export type DistributionType = 'store' | 'internal';
@@ -35,42 +29,32 @@ export type IosVersionAutoIncrement = VersionAutoIncrement | 'buildNumber';
export type AndroidVersionAutoIncrement = VersionAutoIncrement | 'versionCode';
export interface CommonBuildProfile {
// builder
credentialsSource: CredentialsSource;
distribution: DistributionType;
cache?: Omit<Cache, 'clear'>;
releaseChannel?: string;
channel?: string;
developmentClient?: boolean;
prebuildCommand?: string;
autoIncrement?: boolean;
resourceClass?: ResourceClass;
buildArtifactPaths?: string[];
// build environment
env?: Record<string, string>;
node?: string;
yarn?: string;
expoCli?: string;
// credentials
credentialsSource: CredentialsSource;
distribution: DistributionType;
// updates
releaseChannel?: string;
channel?: string;
// build configuration
developmentClient?: boolean;
prebuildCommand?: string;
// versions
autoIncrement?: boolean;
// artifacts
buildArtifactPaths?: string[];
// cache
cache?: Omit<Cache, 'clear'>;
// custom build configuration
config?: string;
env?: Record<string, string>;
}
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
// artifacts
export interface AndroidBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
withoutCredentials?: boolean;
image?: Android.BuilderEnvironment['image'];
ndk?: string;
autoIncrement?: AndroidVersionAutoIncrement;
buildType?: Android.BuildType.APK | Android.BuildType.APP_BUNDLE;
gradleCommand?: string;
/**
* @deprecated use applicationArchivePath
*/
@@ -78,39 +62,22 @@ interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'>
applicationArchivePath?: string;
}
export interface AndroidBuildProfile extends PlatformBuildProfile {
// build environment
image?: Android.BuilderEnvironment['image'];
ndk?: string;
// credentials
withoutCredentials?: boolean;
// build configuration
gradleCommand?: string;
buildType?: Android.BuildType.APK | Android.BuildType.APP_BUNDLE;
// versions
autoIncrement?: AndroidVersionAutoIncrement;
}
export interface IosBuildProfile extends PlatformBuildProfile {
// build environment
export interface IosBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
enterpriseProvisioning?: IosEnterpriseProvisioning;
autoIncrement?: IosVersionAutoIncrement;
simulator?: boolean;
image?: Ios.BuilderEnvironment['image'];
bundler?: string;
fastlane?: string;
cocoapods?: string;
// credentials
enterpriseProvisioning?: IosEnterpriseProvisioning;
// build configuration
simulator?: boolean;
/**
* @deprecated use applicationArchivePath
*/
artifactPath?: string;
applicationArchivePath?: string;
scheme?: string;
buildConfiguration?: string;
// versions
autoIncrement?: IosVersionAutoIncrement;
}
export type BuildProfile<TPlatform extends Platform = Platform> = TPlatform extends Platform.ANDROID
-1
View File
@@ -1,7 +1,6 @@
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
export { EasJson, ProfileType, AppVersionSource } from './types';
export { Platform } from '@expo/eas-build-job';
export {
AndroidVersionAutoIncrement,
BuildProfile,
+3
View File
@@ -13,4 +13,7 @@ export const EasJsonSchema = Joi.object({
}),
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
update: Joi.object({
assetExcludePatterns: Joi.array().items(Joi.string()),
}),
});
+3
View File
@@ -25,4 +25,7 @@ export interface EasJson {
};
build?: { [profileName: string]: EasJsonBuildProfile };
submit?: { [profileName: string]: EasJsonSubmitProfile };
update?: {
assetExcludePatterns?: string[];
};
}
+14 -25
View File
@@ -8,11 +8,6 @@ import { resolveSubmitProfile } from './submit/resolver';
import { SubmitProfile } from './submit/types';
import { EasJson } from './types';
interface EasJsonDeprecationWarning {
message: string[];
docsUrl?: string;
}
export class EasJsonUtils {
public static async getBuildProfileNamesAsync(accessor: EasJsonAccessor): Promise<string[]> {
const easJson = await accessor.readAsync();
@@ -28,26 +23,6 @@ export class EasJsonUtils {
return resolveBuildProfile({ easJson, platform, profileName });
}
public static getBuildProfileDeprecationWarnings(
buildProfile: BuildProfile,
profileName?: string
): EasJsonDeprecationWarning[] {
const warnings: EasJsonDeprecationWarning[] = [];
if (buildProfile.cache?.cacheDefaultPaths !== undefined) {
warnings.push({
message: [
`The "build.${
profileName ?? 'production'
}.cache.cacheDefaultPaths" field in eas.json is deprecated and will be removed in the future.`,
],
docsUrl: 'https://docs.expo.dev/build-reference/caching/#ios-dependencies',
});
}
return warnings;
}
public static async getCliConfigAsync(accessor: EasJsonAccessor): Promise<EasJson['cli'] | null> {
try {
const easJson = await accessor.readAsync();
@@ -73,4 +48,18 @@ export class EasJsonUtils {
const easJson = await accessor.readAsync();
return resolveSubmitProfile({ easJson, platform, profileName });
}
public static async getUpdatesConfigAsync(
accessor: EasJsonAccessor
): Promise<EasJson['update'] | null> {
try {
const easJson = await accessor.readAsync();
return easJson.update ?? null;
} catch (err: any) {
if (err instanceof MissingEasJsonError) {
return null;
}
throw err;
}
}
}
+1 -3
View File
@@ -5,11 +5,9 @@ set -eo pipefail
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
LOCAL_TS_PATH="$ROOT_DIR/packages/eas-cli/src/build/local.ts"
LOCAL_BUILD_PLUGIN_NPM_TAG="eas-cli"
echo "Checking if eas-cli-local-build-plugin upgrade is available"
plugin_version_latest=$(npm info eas-cli-local-build-plugin@$LOCAL_BUILD_PLUGIN_NPM_TAG version)
plugin_version_latest=$(npm info eas-cli-local-build-plugin@latest version)
plugin_version_current=$(grep 'const PLUGIN_PACKAGE_VERSION' $LOCAL_TS_PATH | cut -d"'" -f2)
if [[ "$plugin_version_latest" == "$plugin_version_current" ]]; then
Generated
+36 -63
View File
@@ -1324,10 +1324,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@expo/apple-utils@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-1.2.0.tgz#227d118089a4645b076162f3b513dfc68763ca21"
integrity sha512-MotvQqu8CNVlnl9nsISobXDjEzSMfT7U5Zk+1rhOFoJhaZhlEkARxumUFUgH6cYnK7J2mMd3YxM+G769arkEKQ==
"@expo/apple-utils@0.0.0-alpha.37":
version "0.0.0-alpha.37"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-0.0.0-alpha.37.tgz#e85dea5c4ada8f1e8e26ce9670fb6d38c0092299"
integrity sha512-/v7adya/u6GDBj7py+HsnKmA11VwG34z4/7peJdMfu8M3LZ2G14dsaxEqdndrCAuEyJ6gQ88sggjcjPjzQXdmg==
"@expo/bunyan@^4.0.0":
version "4.0.0"
@@ -1390,13 +1390,21 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/eas-build-job@1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.7.tgz#4c50dbc37faf000c5a5d791e69995bbf0eda39ff"
integrity sha512-LBYXWS/wp6YnyNDUqt+5+k0J7P6gf3/hjf48yJp+BcLabQbqWqAuz7eqPMEDt9bezMX2fEAiYywSBdnRJKo3Ow==
"@expo/eas-build-job@0.2.102":
version "0.2.102"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.102.tgz#6c23388a7ac12d60a46d2a9ef779075f5c1a874b"
integrity sha512-1gav4gHhZCnRDIscd0/sat/fjts543QmexiWXUWDv15uzCxx/NnewN13tcmOwMV0K3Z3agOlJLRPN2t6UvIMAg==
dependencies:
joi "^17.7.0"
semver "^7.3.8"
joi "^17.4.2"
semver "^7.3.5"
"@expo/[email protected]":
version "0.2.103"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.103.tgz#0b587ada854570bc664f59dcd43dc3ca4221d451"
integrity sha512-cstgN8sVIYjq37xHm/4E6jnAlYf1gndvWfuVzRicUjp1TVyGjV9IbilSOxJp8AEqgPZK6+bZOcMRXT634M0Nmg==
dependencies:
joi "^17.4.2"
semver "^7.3.5"
"@expo/[email protected]":
version "0.3.22"
@@ -1433,13 +1441,6 @@
json5 "^2.2.2"
write-file-atomic "^2.3.0"
"@expo/[email protected]":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.0.tgz#41f17209e11aa83eabd5722c0efbc5b80efc7c41"
integrity sha512-i07o2xUsmtpC2mQ3ZHOJRtFEqWTfQabOb3U5Q0Hwip/BvI0yj8Pweu2ExhKLERbGugt4tSSLdQJUcKQbOrue6g==
dependencies:
"@expo/bunyan" "^4.0.0"
"@expo/[email protected]":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@expo/multipart-body-parser/-/multipart-body-parser-1.1.0.tgz#ba402b3047cf14ef17b4d785ec030dd6474ae6e4"
@@ -1572,26 +1573,6 @@
dependencies:
cross-spawn "^7.0.3"
"@expo/spawn-async@^1.7.0":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58"
integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==
dependencies:
cross-spawn "^7.0.3"
"@expo/[email protected]":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.8.tgz#06fe9ca5bde449255661cf090666e70dd2ae5345"
integrity sha512-1SxHOtfLwCdKxOUtbXjz548JQlkkVoNjaydNmo7zZgPvD/P4aviaOSCL3StUpQuWNlcERLzjOn8/NgvsdDmMyw==
dependencies:
"@expo/logger" "1.0.0"
"@expo/spawn-async" "^1.7.0"
arg "^5.0.2"
joi "^17.7.0"
this-file "^2.0.3"
uuid "^9.0.0"
yaml "^2.2.1"
"@expo/[email protected]":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@expo/timeago.js/-/timeago.js-1.0.0.tgz#8fb2b17e93e7a8d28387a4d292af12e071040045"
@@ -4004,6 +3985,13 @@
component-type "^1.2.1"
join-component "^1.1.0"
"@sideway/address@^4.1.0":
version "4.1.2"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.2.tgz#811b84333a335739d3969cfc434736268170cad1"
integrity sha512-idTz8ibqWFrPU8kMirL0CoPH/A29XOzzAzpyN3zQ4kAWnzmNfFmRaoMNN6VI8ske5M73HZyhIaW4OuSFIdM4oA==
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/address@^4.1.3":
version "4.1.3"
resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27"
@@ -4011,10 +3999,10 @@
dependencies:
"@hapi/hoek" "^9.0.0"
"@sideway/formula@^3.0.0", "@sideway/formula@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.1.tgz#80fcbcbaf7ce031e0ef2dd29b1bfc7c3f583611f"
integrity sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==
"@sideway/formula@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@sideway/formula/-/formula-3.0.0.tgz#fe158aee32e6bd5de85044be615bc08478a0a13c"
integrity sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==
"@sideway/pinpoint@^2.0.0":
version "2.0.0"
@@ -4859,11 +4847,6 @@ arg@^4.1.0:
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
@@ -9345,15 +9328,15 @@ [email protected]:
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
joi@^17.7.0:
version "17.8.3"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.8.3.tgz#d772fe27a87a5cda21aace5cf11eee8671ca7e6f"
integrity sha512-q5Fn6Tj/jR8PfrLrx4fpGH4v9qM6o+vDUfD4/3vxxyg34OmKcNqYZ1qn2mpLza96S8tL0p0rIw2gOZX+/cTg9w==
joi@^17.4.2:
version "17.4.2"
resolved "https://registry.yarnpkg.com/joi/-/joi-17.4.2.tgz#02f4eb5cf88e515e614830239379dcbbe28ce7f7"
integrity sha512-Lm56PP+n0+Z2A2rfRvsfWVDXGEWjXxatPopkQ8qQ5mxCEhwHG+Ettgg5o98FFaxilOxozoa14cFhrE/hOzh/Nw==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.3"
"@sideway/formula" "^3.0.1"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
join-component@^1.1.0:
@@ -12841,11 +12824,6 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
this-file@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/this-file/-/this-file-2.0.3.tgz#13bd2bcfbab2ce86a37a15689df1d14861032b1b"
integrity sha512-IdMH1bUkVJdJjM7o8v83Mv4QvVPdkAofur20STl2Bbw9uMuuS/bT/PZURkEdZsy9XC/1ZXWgZ1wIL9nvouGaEg==
through2@^2.0.0:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -13359,7 +13337,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
[email protected], uuid@^9.0.0:
[email protected]:
version "9.0.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
@@ -13799,11 +13777,6 @@ yaml@^1.10.0, yaml@^1.7.2:
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
yaml@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
[email protected], yargs-parser@^20.2.2:
version "20.2.4"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"