Compare commits

...
Author SHA1 Message Date
Cedric van Putten cad3dca5df v3.8.1 2023-03-16 14:09:02 +01:00
Cedric van Putten 2ef6e79395 [eas-cli] Add missing changelog entry 2023-03-16 14:03:51 +01:00
Cedric van Putten 44270814a7 [eas-cli] Add test case to ensure public config is used as update (#1745)
* [eas-cli] Add test case to ensure public config is used as update

* [eas-cli] Use the public config when creating updates
2023-03-15 23:12:47 +01:00
Radosław Krzemień 95155681db [eas-cli] [ENG-6945] Don't use capitalised base error messages (#1744)
* Capitalize command id

In case the base error message is not provided and the command id starts with lower case character it will be capitalized to unify the behaviour with other cases

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Decapitalize command id

After some discussion it was agreed to decapitalize the base error messages instead, and unify the behaviour this way

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Remove redundant decapitalisation

Seems like `oclif` guarantees the command ids to be lower case in which case the explicit decapitalisation is redundant

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* update CHANGELOG.md
2023-03-14 15:42:41 +01:00
Radosław KrzemieńandWojciech Kozyra 53fd599d0a [eas-cli] [ENG-6945] Unify error display method (#1738)
* Unify error display method

Overriden the `Command`'s `catch` method to catch all the errors thrown by the commands, log the error message there the same way as it was previously handled in `src/build/build.ts` and re-throw generic `Error` instance with predefined base error message.
To avoid duplication, old `Log.error()` calls have been removed from `src/build/build.ts`.
Also added tests to see if the `catch()` method works as intended.

* Adjust base error messages

Base error messages have been adjusted after @dsokal suggested that the error may be caused by something different from a request and displaying message about a failed request is misleading

* update CHANGELOG.md

* Add test for unclean error message

Checks if the `[GraphQL] ` prefix gets removed when logging the error message

* Update handling GraphQL errors

Post-review update to how graphQL errors are handled

* Print different message for graphQL errors

When a graphQL error is caught a different base error message is printed

* Print original error to debug

When a graphQL error is caught and the message is cleaned, an original message is printed to debug (or just the same message otherwise)

* Add custom error classes

Created some custom error classes for known server side defined errors

* Log the error, not the message

Changed the debug log to log the error object, not the error message

* Fix

Fixed testing leftover

* Change phrasing

Edited the phrasing in default messages related to EAS Build

* Update if condition

Changed the if condition after review comment

* Create error base class

Created a new base class for the errors expected in build.ts, so they can be handled separately in `catch()`

* Move EasCommand properties

Moved the class properties above the class methods

* Remove helper method

Removed the helper method after review comment

* Change const type

Updated the type of SERVER_SIDE_DEFINED_ERRORS constant

* Change default base error message

Set the default base error message to `<command_name> command failed.`

* Remove comments

Removed leftover comments

* Change optional property

Changed empty string to optional property

* Post-review tweaks

Self-explanatory

* Fix failing test

One of tests started failing after changing the coalescing operators (|| -> ??). This fixes that test

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Remove error class

Removed `GenericGraphQLError` class after @wkozyra95 comment, since atm there's no need to handle it differently than a generic `Error`

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Move `EasCommandError` base class

Moved `EasCommandError` base class after @wkozyra95 comment. The class is not specific to `Build` command and so it has been moved to `commandUtils` directory

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Make message mandatory

Made `message` parameter in `EasCommandError`s mandatory, to remove need for providing a fallback string and making the server the source of truth for the errors messages

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Update packages/eas-cli/src/commandUtils/EasCommand.ts

Co-authored-by: Wojciech Kozyra <[email protected]>

* Move `baseGraphQLErrorMessage` property

Made the property into a constant in a global scope, since atm it is not changed in any of the subclasses

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Suppress ESLint warning

ESLint erroneously complains about the constructor of `EasCommandError` to be useless, which is not true is the constructor of `Error` parent class allows for optional `message` parameter and the constructor of `EasCommandError` does not

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Add error handling tests

Added a bunch of new tests related to the `handleBuildRequestError` function

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

* Directly export `handleBuildRequestError`

Dropped the `testExports` convention after team discussion

See: https://linear.app/expo/issue/ENG-6945/improve-eas-cli-error-logging

---------

Co-authored-by: Wojciech Kozyra <[email protected]>
2023-03-14 11:26:27 +01:00
dependabot[bot] b51201d010 Bump @sideway/formula from 3.0.0 to 3.0.1 (#1742)
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-13 12:52:53 +01:00
Expo CI f86c079733 update CHANGELOG.md 2023-03-13 11:51:39 +00:00
Szymon Dziedzic 843ec7407b v3.8.0 2023-03-13 12:44:41 +01:00
Szymon Dziedzic 537cd7872a [ENG-7709][eas-cli][eas-json] add m-medium and m-large resource classes (#1739)
* [eas-cli][eas-json] add `m-medium` and `m-large` resource classes

* add deprecation warning for m1-medium resource class

* update deprecation messages

* apply suggested changes

* update CHANGELOG.md
2023-03-13 12:33:48 +01:00
Dominik Sokal 007c40bed4 [gha] send slack notification on failed tests (#1720) 2023-02-24 16:26:23 +01:00
Dominik Sokal ba9f8d8ddd Update CHANGELOG.md 2023-02-24 09:54:39 +01:00
Expo CI e4bd933e81 update CHANGELOG.md 2023-02-24 08:27:04 +00:00
Evan Bacon a6434bf6b8 v3.7.2 2023-02-24 02:18:15 -06:00
Evan Bacon 19bad84109 [eas-cli] bump apple-utils to support hashcash for authentication (#1719)
* bump apple-utils to support hashcash for authentication

* update CHANGELOG.md
2023-02-24 02:16:06 -06:00
Expo CI 5d9f8f9d76 update CHANGELOG.md 2023-02-23 21:08:09 +00:00
Brent Vatne d9d884d211 v3.7.1 2023-02-23 12:59:33 -08:00
Brent Vatne f902bb845b Add changelog entry 2023-02-23 12:59:05 -08:00
Brent Vatne 03c76c1e0e [eas-cli] Bump apple-utils 2023-02-23 12:58:33 -08:00
Expo CI 1d0d329cdb update CHANGELOG.md 2023-02-23 10:48:34 +00:00
Dominik Sokal 7ed1efb7d6 v3.7.0 2023-02-23 11:23:55 +01:00
Dominik Sokal b7bfec0366 [ENG-7565] add basic support for custom builds (#1710)
* custom builds - wip

* upgrade @expo/eas-build-job

* [eas-json] clean up build profile schema + types

* validate if custom build config exists

* don't configure credentials for custom builds

* set buildMode in metadata

* address PR feedback

* regenerate graphql code
2023-02-23 10:16:22 +01:00
Frederick Widjaja a5308f0d37 Support variant detection for package*UniversalApk Gradle Commands (#1708)
* Support variant detection for package*UniversalApk Gradle Commands

* Update CHANGELOG.md
2023-02-23 09:31:54 +01:00
Cedric van Putten 3ad644bb9d [eas-cli] Provide more context about uploaded assets when publishing (#1488) 2023-02-22 17:39:44 -08:00
Szymon Dziedzic 233e5451a8 update changelog 2023-02-20 11:40:47 +01:00
Szymon Dziedzic ce954cdecc v3.6.1 2023-02-20 11:19:14 +01:00
Szymon Dziedzic 59bd1caf98 [ENG-7376][eas-cli] link to build phase in error message (#1699)
* [eas-cli] link to build phase in error message

* add better fallback

* use fallback for all links

* use terminalLink.isSupported

* improvements after code review

* use old fallback

* update CHANGELOG.md
2023-02-17 15:09:54 +00:00
dependabot[bot] 86877fdda3 Bump undici from 5.8.2 to 5.19.1 (#1706)
Bumps [undici](https://github.com/nodejs/undici) from 5.8.2 to 5.19.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.8.2...v5.19.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-17 09:00:13 +00:00
Will Schurman 9299c9b7cc [eas-cli] Clarify missing owner field error message for Robot users (#1702)
* [eas-cli] Clarify missing owner field error message for Robot users

* update CHANGELOG.md

* Address comment
2023-02-16 18:34:25 +00:00
Szymon Dziedzic 64fb2e5b3a [ci] fix release script (#1704)
* fix release script

* apply suggested changes
2023-02-16 11:19:53 +00:00
Szymon Dziedzic 91fdbac773 [eas-cli] display build message data when picking build in submit, build:run and build:resign commands (#1700)
* [eas-cli] display build message data when picking build in `submit`, `build:run` and `build:resign` commands

* display only 200 first characters of message if it exists

* print only defined values

* add '...' to message if trimmed

* use array instead of object

* update CHANGELOG.md
2023-02-16 10:36:23 +00:00
Szymon Dziedzic 507eadb914 [ci] commit release changelog update using expo bot PAT (#1697) 2023-02-15 16:09:18 +00:00
Wojciech Kozyra f1cde7a291 [eas-cli] Fix README.md (#1701) 2023-02-15 13:01:56 +00:00
Wojciech Kozyra 8b9ad47d16 [eas-cli] Validate platform for local builds earlier (#1698)
* [eas-cli] Validate platform for local builds earlier

* update CHANGELOG.md
2023-02-15 11:28:19 +00:00
Wojciech Kozyra 192984b47d [eas-cli] Disable analytics for environments behind proxy (#1696)
* [eas-cli] Disable analytics for environments behind proxy

* update CHANGELOG.md
2023-02-15 09:46:55 +00:00
Szymon Dziedzic 2c36810ce7 update changelog 2023-02-14 14:31:10 +01:00
Szymon Dziedzic bfa63f95fe v3.6.0 2023-02-14 13:32:03 +01:00
Szymon Dziedzic c414b9f975 [ENG-7389][eas-json] add Xcode 14.2 image to schema (#1693)
* add Xcode 14.2 image to schema

* update CHANGELOG.md
2023-02-14 11:21:07 +00:00
Raul Riera 6946a085cb feat: return the build message when one is available (#1691)
* Return the message when available

* Update CHANGELOG.md

* run generate-graphql-code
2023-02-13 15:56:41 +00:00
54 changed files with 4219 additions and 690 deletions

No files matched your search

@@ -0,0 +1,4 @@
{
"printWidth": 100,
"singleQuote": true
}
@@ -0,0 +1,53 @@
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: ''
@@ -0,0 +1,155 @@
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' };
};
@@ -0,0 +1,12 @@
{
"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"
}
}
@@ -0,0 +1,8 @@
# 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==
+4 -2
View File
@@ -161,6 +161,8 @@ jobs:
needs: publish-to-npm
steps:
- uses: actions/checkout@v2
with:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 18
@@ -197,9 +199,9 @@ jobs:
env:
SLACK_CHANNEL: eas-cli
SLACK_COLOR: ${{ job.status }}
SLACK_ICON: https://github.com/expo-ci.png?size=48
SLACK_ICON: https://github.com/expo-bot.png?size=48
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
SLACK_USERNAME: 'Expo CI'
SLACK_USERNAME: 'EAS CLI CI'
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_FOOTER: ''
+12
View File
@@ -39,3 +39,15 @@ 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 }}
+53
View File
@@ -8,8 +8,59 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
### 🐛 Bug fixes
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
### 🧹 Chores
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
### 🎉 New features
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
### 🐛 Bug fixes
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
### 🐛 Bug fixes
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
### 🎉 New features
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
### 🐛 Bug fixes
- Disable analytics when running behind a proxy. ([#1696](https://github.com/expo/eas-cli/pull/1696) by [@wkozyra95](https://github.com/wkozyra95))
- Clarify missing owner field error message for Robot users. ([#1702](https://github.com/expo/eas-cli/pull/1702) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Validate the platform for local builds earlier. ([#1698](https://github.com/expo/eas-cli/pull/1698) by [@wkozyra95](https://github.com/wkozyra95))
- Display build message when picking build in `eas submit`, `eas build:resign`, and `eas build:run` commands. ([#1700](https://github.com/expo/eas-cli/pull/1700) by [@szdziedzic](https://github.com/szdziedzic))
- Add a link directly to a build phase logs in the EAS CLI build error message. ([#1699](https://github.com/expo/eas-cli/pull/1699) by [@szdziedzic](https://github.com/szdziedzic))
## [3.6.0](https://github.com/expo/eas-cli/releases/tag/v3.6.0) - 2023-02-14
### 🎉 New features
- Use `sdkVersion` as default runtime version policy when running `eas update:configure`. ([#1669](https://github.com/expo/eas-cli/pull/1669) by [@jonsamp](https://github.com/jonsamp))
- Warn when project ID but no owner specified and mismatch logged in user. ([#1667](https://github.com/expo/eas-cli/pull/1667) by [@wschurman](https://github.com/wschurman))
- Return the build `message` if one is available. ([#1691](https://github.com/expo/eas-cli/pull/1691) by [@raulriera](https://github.com/raulriera))
### 🐛 Bug fixes
@@ -17,6 +68,8 @@ This is the log of notable changes to EAS CLI and related packages.
### 🧹 Chores
- Add Xcode 14.2 image to the VSCode schema. ([#1693](https://github.com/expo/eas-cli/pull/1693) by [@szdziedzic](https://github.com/szdziedzic))
## [3.5.2](https://github.com/expo/eas-cli/releases/tag/v3.5.2) - 2023-02-05
### 🐛 Bug fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "3.5.2",
"version": "3.8.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+1 -1
View File
@@ -14,7 +14,7 @@
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
"lint-changelog": "./scripts/bin/run lint-changelog",
"release": "./scripts/bin/run release",
"release": "lerna run rebuild && ./scripts/bin/run release",
"test": "jest",
"clean": "lerna run clean && rm -rf node_modules coverage"
},
File diff suppressed because it is too large. Load diff
+1146 -330
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,20 +1,20 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "3.5.2",
"version": "3.8.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "0.0.0-alpha.37",
"@expo/apple-utils": "1.1.0",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "7.0.3",
"@expo/config-plugins": "5.0.4",
"@expo/config-types": "47.0.0",
"@expo/eas-build-job": "0.2.103",
"@expo/eas-json": "3.5.0",
"@expo/eas-build-job": "0.2.106",
"@expo/eas-json": "3.8.0",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
@@ -202,7 +202,8 @@
"repository": "expo/eas-cli",
"scripts": {
"postpack": "rimraf oclif.manifest.json",
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
"prepack": "yarn rebuild && node ./scripts/prepack.js",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
@@ -133,7 +133,9 @@ export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
}
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
const analyticsEnabled =
!process.env.https_proxy && // disable analytics if running behind proxy
(await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true));
if (!analyticsEnabled) {
return new NoOpAnalytics();
}
@@ -0,0 +1,362 @@
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);
}
});
});
});
});
});
+2 -1
View File
@@ -124,7 +124,8 @@ export async function prepareAndroidBuildAsync(
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
const isCustomBuild = ctx.buildProfile.config !== undefined;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
}
async function ensureAndroidCredentialsAsync(
@@ -1,7 +1,12 @@
import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -22,6 +27,8 @@ 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,6 +12,7 @@ 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';
@@ -49,11 +50,15 @@ export async function prepareJobAsync(
}
: {};
let buildType = ctx.buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
let buildType = buildProfile.buildType;
if (!buildType && !buildProfile.gradleCommand && 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,
@@ -75,8 +80,8 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
@@ -89,10 +94,15 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
prebuildCommand: buildProfile.prebuildCommand,
},
mode: BuildMode.BUILD,
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
+28 -23
View File
@@ -10,6 +10,7 @@ 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,
@@ -36,6 +37,15 @@ 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';
@@ -161,38 +171,33 @@ export async function prepareBuildRequestForPlatformAsync<
};
}
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',
];
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,
};
function handleBuildRequestError(error: any, platform: Platform): never {
export function handleBuildRequestError(error: any, platform: Platform): never {
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
if (SERVER_SIDE_DEFINED_ERRORS.includes(error?.graphQLErrors?.[0]?.extensions?.errorCode)) {
Log.error(error?.graphQLErrors?.[0]?.message);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
) {
Log.error(
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
throw new EasBuildDownForMaintenanceError(
`EAS Build is down for maintenance. Try again later. Check ${link(
'https://status.expo.dev/'
)} for updates.`
);
throw new Error('Build request failed.');
} else if (
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
) {
Log.error(
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
throw new EasBuildTooManyPendingBuildsError(
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
throw new Error('Build request failed.');
} else if (error?.graphQLErrors) {
Log.error(
throw new Error(
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
);
}
@@ -615,7 +620,7 @@ async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Pr
for (const profileName of profileNames) {
easJsonRawObject.build[profileName].ios = {
...easJsonRawObject.build[profileName].ios,
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
};
}
return easJsonRawObject;
+6 -6
View File
@@ -54,18 +54,18 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
developmentClient: true,
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
},
@@ -86,18 +86,18 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
ios: {
buildConfiguration: 'Debug',
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
preview: {
distribution: 'internal',
ios: {
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
production: {
ios: {
resourceClass: ResourceClass.M1_MEDIUM,
resourceClass: ResourceClass.M_MEDIUM,
},
},
},
+15
View File
@@ -0,0 +1,15 @@
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 {}
+7 -2
View File
@@ -91,11 +91,16 @@ export function transformIosEnterpriseProvisioning(
}
}
export function transformBuildMode(buildMode: Metadata['buildMode']): BuildMode {
// TODO: check what in metadata
export function transformBuildMode(buildMode: string): BuildMode {
if (buildMode === 'build') {
return BuildMode.Build;
} else {
} else if (buildMode === 'resign') {
return BuildMode.Resign;
} else if (buildMode === 'custom') {
return BuildMode.Custom;
} else {
throw new Error(`Unsupported build mode: ${buildMode}`);
}
}
@@ -55,5 +55,6 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator;
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
}
+9 -2
View File
@@ -2,7 +2,12 @@ import { Ios } from '@expo/eas-build-job';
import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { transformBuildTrigger, transformProjectArchive, transformWorkflow } from '../graphql';
import {
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -12,7 +17,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: transformIosSecrets(job.secrets),
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment,
cache: job.cache,
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
@@ -24,6 +29,8 @@ export function transformJob(job: Ios.Job): IosJobInput {
developmentClient: job.developmentClient,
simulator: job.simulator,
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
};
}
+32 -21
View File
@@ -7,12 +7,14 @@ 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';
@@ -33,10 +35,11 @@ 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 username = getUsername(ctx.exp, ctx.user);
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
for (const targetName of targetNames) {
@@ -44,38 +47,41 @@ 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: 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,
image: buildProfile.image,
node: buildProfile.node,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
...ctx.buildProfile.cache,
...buildProfile.cache,
clear: ctx.clearCache,
},
secrets: {
buildCredentials,
},
releaseChannel: ctx.buildProfile.releaseChannel,
updates: { channel: ctx.buildProfile.channel },
developmentClient: ctx.buildProfile.developmentClient,
simulator: ctx.buildProfile.simulator,
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
scheme: jobData.buildScheme,
buildConfiguration: ctx.buildProfile.buildConfiguration,
applicationArchivePath:
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -83,10 +89,15 @@ export async function prepareJobAsync(
},
}),
experimental: {
prebuildCommand: ctx.buildProfile.prebuildCommand,
prebuildCommand: buildProfile.prebuildCommand,
},
mode: BuildMode.BUILD,
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
};
return sanitizeJob(job);
}
+2 -1
View File
@@ -1,5 +1,5 @@
import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { BuildMode, 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,6 +57,7 @@ export async function collectMetadataAsync<T extends Platform>(
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
};
return sanitizeMetadata(metadata);
}
+23 -12
View File
@@ -99,10 +99,6 @@ export async function listAndSelectBuildOnAppAsync(
}
}
function formatBuildChoiceValue(value: string | undefined | null): string {
return value ? chalk.bold(value) : 'Unknown';
}
function createBuildToPartialChoiceMaker(
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
) {
@@ -119,19 +115,34 @@ function createBuildToPartialChoiceMaker(
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: 'Unknown';
: null;
const descriptionItems: { name: string; value: string | null }[] = [
{ name: 'Version', value: build.appVersion ? chalk.bold(build.appVersion) : null },
{ name: 'Commit', value: formattedCommitData },
{
name: 'Message',
value: build.message
? chalk.bold(
build.message.length > 200 ? `${build.message.slice(0, 200)}...` : build.message
)
: null,
},
{
name: build.platform === AppPlatform.Ios ? 'Build number' : 'Version code',
value: build.appBuildVersion ? chalk.bold(build.appBuildVersion) : null,
},
];
const filteredDescriptionArray: string[] = descriptionItems
.filter(item => item.value)
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
return {
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
`${fromNow(new Date(build.completedAt))} ago`
)})`,
description: [
`\t${chalk.bold(`Version:`)} ${formatBuildChoiceValue(build.appVersion)}`,
`\t${chalk.bold(
build.platform === AppPlatform.Ios ? 'Build number:' : 'Version code:'
)} ${formatBuildChoiceValue(build.appBuildVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n'),
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
disabled: selectPromptDisabledFunction?.(build),
};
};
@@ -31,6 +31,7 @@ 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';
@@ -118,6 +119,7 @@ export async function runBuildAndSubmitAsync(
for (const buildProfile of buildProfiles) {
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
}
const startedBuilds: {
@@ -38,6 +38,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
},
},
{
label: 'Message',
value: build.message,
},
{
label: 'Distribution',
value: build.distribution?.toLowerCase(),
@@ -15,6 +15,32 @@ import Log, { learnMore, link } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
function terminalLinkFallback(url: string, text: string): string {
return `${text} (${url})`;
}
const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) => string> = {
EAS_BUILD_UNKNOWN_FASTLANE_ERROR: build =>
`The ${link(getBuildLogsUrl(build, 'run-fastlane'), {
text: '"Run fastlane"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-fastlane'), '"Run fastlane"'),
})} step failed with an unknown error. Refer to the ${link(
getBuildLogsUrl(build, 'xcode-logs'),
{
text: '"Xcode logs"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'xcode-logs'), '"Xcode logs"'),
}
)} phase for additional, more detailed logs`,
EAS_BUILD_UNKNOWN_GRADLE_ERROR: build =>
`Gradle build failed with unknown error. See logs for the ${link(
getBuildLogsUrl(build, 'run-gradlew'),
{
text: '"Run gradlew"',
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-gradlew'), '"Run gradlew"'),
}
)} phase for more information.`,
};
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
@@ -31,7 +57,6 @@ 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');
@@ -42,8 +67,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]} ${
@@ -51,11 +76,12 @@ function printBuildResult(build: BuildFragment): void {
} build failed${userError ? ':' : ''}`
);
if (userError) {
printUserError(userError);
printUserError(userError, build);
}
return;
}
if (build.status === BuildStatus.Canceled) {
Log.addNewLineIfNone();
Log.error(
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
@@ -65,6 +91,7 @@ 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
@@ -82,9 +109,14 @@ 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 ?? '';
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
Log.log(`${link(url)}`);
const url = build.artifacts?.buildUrl;
if (url) {
Log.addNewLineIfNone();
Log.log(
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
);
Log.log(link(url));
}
}
}
@@ -105,9 +137,22 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
}
}
export function printUserError(error: BuildError): void {
Log.error(error.message);
if (error.docsUrl) {
Log.error(learnMore(error.docsUrl, { dim: false }));
export function printUserError(error: BuildError, build: BuildFragment): void {
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build);
if (maybeErrorMessageOverride) {
Log.error(maybeErrorMessageOverride);
} else {
Log.error(error.message);
if (error.docsUrl) {
Log.error(learnMore(error.docsUrl, { dim: false }));
}
}
}
function maybeGetErrorMessageOverride(error: BuildError, build: BuildFragment): string | null {
if (!(error.errorCode in errorCodeToErrorMessageOverride)) {
return null;
}
return errorCodeToErrorMessageOverride[error.errorCode](build);
}
@@ -8,24 +8,30 @@ 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.IosM1Large,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosM1Medium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosM1Large,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
};
const androidResourceClassToBuildResourceClassMapping: Record<
Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>,
AndroidResourceClass,
BuildResourceClass
> = {
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
@@ -70,15 +76,7 @@ function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): Bui
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
return androidResourceClassToBuildResourceClassMapping[
resourceClass as Exclude<
ResourceClass,
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
>
];
return androidResourceClassToBuildResourceClassMapping[resourceClass as AndroidResourceClass];
}
async function resolveIosResourceClassAsync(
@@ -89,8 +87,20 @@ async function resolveIosResourceClassAsync(
const resourceClass =
selectedResourceClass ?? (await resolveIosDefaultRequestedResourceClassAsync(exp, projectDir));
if (resourceClass === ResourceClass.M1_EXPERIMENTAL) {
Log.warn(`Resource class ${chalk.bold('m1-experimental')} is deprecated.`);
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'm-medium'
)} instead.`
);
}
if (resourceClass === ResourceClass.M1_LARGE) {
Log.warn(
`Resource class ${chalk.bold('m1-large')} is deprecated. Use ${chalk.bold(
'm-large'
)} instead.`
);
}
return iosResourceClassToBuildResourceClassMapping[resourceClass];
@@ -106,7 +116,7 @@ async function resolveIosDefaultRequestedResourceClassAsync(
(sdkVersion && semver.satisfies(sdkVersion, '>=48')) ||
(reactNativeVersion && semver.satisfies(reactNativeVersion, '>=0.71.0'))
) {
return ResourceClass.M1_MEDIUM;
return ResourceClass.M_MEDIUM;
} else {
return ResourceClass.DEFAULT;
}
+4 -2
View File
@@ -10,12 +10,14 @@ export function getProjectDashboardUrl(accountName: string, projectName: string)
).toString();
}
export function getBuildLogsUrl(build: BuildFragment): string {
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string {
const { project } = build;
const url =
project.__typename !== 'App'
? `/builds/${build.id}`
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}${
hash ? `#${hash}` : ''
}`;
return new URL(url, getExpoWebsiteBaseUrl()).toString();
}
+32 -10
View File
@@ -1,4 +1,5 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import nullthrows from 'nullthrows';
import {
@@ -6,6 +7,7 @@ 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';
@@ -16,6 +18,7 @@ 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 {
@@ -33,6 +36,8 @@ 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 = {
/**
@@ -103,6 +108,18 @@ 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.
*/
@@ -132,20 +149,10 @@ 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);
}
@@ -175,4 +182,19 @@ 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,4 +1,7 @@
import { CombinedError } from '@urql/core';
import { AnalyticsWithOrchestration, createAnalyticsAsync } from '../../analytics/AnalyticsManager';
import Log from '../../log';
import SessionManager from '../../user/SessionManager';
import EasCommand from '../EasCommand';
@@ -11,6 +14,7 @@ jest.mock('../../analytics/AnalyticsManager', () => {
createAnalyticsAsync: jest.fn(),
};
});
jest.mock('../../log');
let originalProcessArgv: string[];
@@ -40,7 +44,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;
};
@@ -96,5 +100,71 @@ 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.');
}
});
});
});
});
@@ -136,6 +136,54 @@ describe(getProjectIdAsync, () => {
);
});
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
__typename: 'Robot',
id: 'robot_id',
accounts: [
{
id: 'account_id_1',
name: 'notnotbrent',
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
},
{
id: 'account_id_2',
name: 'dominik',
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
},
],
isExpoAdmin: false,
featureGates: {},
},
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
});
const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManagerRobot,
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false }
)
).rejects.toThrow(
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
});
it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
@@ -103,15 +103,28 @@ export async function getProjectIdAsync(
const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
if (actorUsername) {
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
} else {
// robot caller
throw new Error(
`Project config: Owner of project identified by "extra.eas.projectId" (${
appForProjectId.ownerAccount.name
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
appForProjectId.ownerAccount.name
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
'https://expo.fyi/eas-project-id'
)}`
);
}
}
if (exp.slug && exp.slug !== appForProjectId.slug) {
@@ -0,0 +1,7 @@
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);
}
}
@@ -227,6 +227,13 @@ export default class Build extends EasCommand {
});
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
} else if (
!['linux', 'darwin'].includes(process.platform) &&
requestedPlatform === RequestedPlatform.Android
) {
Errors.error('Unsupported platform, macOS or Linux is required to build apps for Android', {
exit: 1,
});
}
}
@@ -1,4 +1,11 @@
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
import {
AppJSONConfig,
ExpoConfig,
GetConfigOptions,
PackageJSONConfig,
Platform,
getConfig,
} from '@expo/config';
import { Updates } from '@expo/config-plugins';
import { vol } from 'memfs';
import path from 'path';
@@ -46,7 +53,7 @@ jest.mock('../../../graphql/queries/AppQuery');
jest.mock('../../../graphql/queries/UpdateQuery');
jest.mock('../../../ora', () => ({
ora: () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
}),
}));
jest.mock('../../../project/publish', () => ({
@@ -58,7 +65,10 @@ jest.mock('../../../project/publish', () => ({
}));
describe(UpdatePublish.name, () => {
afterEach(() => vol.reset());
afterEach(() => {
vol.reset();
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockClear();
});
// Deprecated and split to a new command: update:republish
it('errors with --republish', async () => {
@@ -136,14 +146,59 @@ 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 = 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;
} = {}): { projectId: string } {
expoConfig?: Partial<ExpoConfig>;
} = {}): { projectId: string; appJson: AppJSONConfig } {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
version: '0.1.0',
@@ -163,6 +218,7 @@ function mockTestProject({
projectId: configuredProjectId,
},
},
...expoConfig,
},
};
@@ -180,11 +236,27 @@ function mockTestProject({
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
jest
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
.mockResolvedValue(async () => ({
exp: mockManifest.exp,
projectDir: projectRoot,
projectId: configuredProjectId,
}));
.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,
@@ -199,7 +271,7 @@ function mockTestProject({
ownerAccount: jester.accounts[0],
});
return { projectId: configuredProjectId };
return { projectId: configuredProjectId, appJson: appJSON };
}
/** Create a new in-memory export of the project */
@@ -246,6 +318,8 @@ function mockTestExport({
uniqueAssetCount: platforms.length,
uniqueUploadedAssetCount: platforms.length,
assetLimitPerUpdateGroup: 9001,
launchAssetCount: 2,
uniqueUploadedAssetPaths: [],
});
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
+29 -5
View File
@@ -211,7 +211,7 @@ export default class UpdatePublish extends EasCommand {
projectId,
});
const { exp } = await getDynamicProjectConfigAsync({});
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
let realizedPlatforms: PublishPlatform[] = [];
@@ -318,10 +318,34 @@ export default class UpdatePublish extends EasCommand {
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
const uploadAssetSuccessMessage = uploadedAssetCount
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
: `Uploaded: No changes detected`;
assetSpinner.succeed(uploadAssetSuccessMessage);
// NOTE(cedric): we assume that bundles are always uploaded, and always are part of
// `uploadedAssetCount`, perferably we don't assume. For that, we need to refactor the
// `uploadAssetsAsync` and be able to determine asset type from the uploaded assets.
const uploadedBundleCount = uploadResults.launchAssetCount;
const uploadedNormalAssetCount = Math.max(0, uploadedAssetCount - uploadedBundleCount);
const reusedNormalAssetCount = uploadResults.uniqueAssetCount - uploadedNormalAssetCount;
assetSpinner.stop();
Log.withTick(
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
);
if (uploadedNormalAssetCount === 0) {
Log.withTick(`Uploading assets skipped - no new assets found`);
} else {
let message = `Uploaded ${uploadedNormalAssetCount} ${
uploadedNormalAssetCount === 1 ? 'asset' : 'assets'
}`;
if (reusedNormalAssetCount > 0) {
message += ` (reused ${reusedNormalAssetCount} ${
reusedNormalAssetCount === 1 ? 'asset' : 'assets'
})`;
}
Log.withTick(message);
}
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
}
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
+193 -65
View File
@@ -115,6 +115,8 @@ export type Account = {
offers?: Maybe<Array<Offer>>;
/** Owning User of this account if personal account */
owner?: Maybe<User>;
/** Owning UserActor of this account if personal account */
ownerUserActor?: Maybe<UserActor>;
pushSecurityEnabled: Scalars['Boolean'];
/** @deprecated Legacy access tokens are deprecated */
requiresAccessTokenForPushSecurity: Scalars['Boolean'];
@@ -126,11 +128,18 @@ export type Account = {
subscription?: Maybe<SubscriptionDetails>;
/** @deprecated No longer needed */
subscriptionChangesPending?: Maybe<Scalars['Boolean']>;
/** Coalesced project activity for an app using pagination */
timelineActivity: TimelineActivityConnection;
/** @deprecated See isCurrent */
unlimitedBuilds: Scalars['Boolean'];
updatedAt: Scalars['DateTime'];
/** Account query object for querying EAS usage metrics */
usageMetrics: AccountUsageMetrics;
/**
* Owning UserActor of this account if personal account
* @deprecated Deprecated in favor of ownerUserActor
*/
userActorOwner?: Maybe<UserActor>;
/** Pending user invitations for this account */
userInvitations: Array<UserInvitation>;
/** Actors associated with this account and permissions they hold */
@@ -262,6 +271,19 @@ export type AccountSnacksArgs = {
offset: Scalars['Int'];
};
/**
* An account is a container owning projects, credentials, billing and other organization
* data and settings. Actors may own and be members of accounts.
*/
export type AccountTimelineActivityArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<TimelineActivityFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type AccountDataInput = {
name: Scalars['String'];
};
@@ -493,6 +515,7 @@ export type AccountUsageMetricAndCost = {
/** The limit, in units, allowed by this plan */
limit: Scalars['Float'];
metricType: UsageMetricType;
service: EasService;
serviceMetric: EasServiceMetric;
/** Total cost of this particular metric, in cents */
totalCost: Scalars['Float'];
@@ -508,6 +531,7 @@ export type AccountUsageMetrics = {
export type AccountUsageMetricsByBillingPeriodArgs = {
date: Scalars['DateTime'];
service?: InputMaybe<EasService>;
};
@@ -788,6 +812,7 @@ export type AndroidJobInput = {
buildType?: InputMaybe<AndroidBuildType>;
builderEnvironment?: InputMaybe<AndroidBuilderEnvironmentInput>;
cache?: InputMaybe<BuildCacheInput>;
customBuildConfig?: InputMaybe<CustomBuildConfigInput>;
developmentClient?: InputMaybe<Scalars['Boolean']>;
experimental?: InputMaybe<Scalars['JSONObject']>;
gradleCommand?: InputMaybe<Scalars['String']>;
@@ -920,6 +945,7 @@ export type App = Project & {
/** ios.appStoreUrl field from most recent classic update manifest */
appStoreUrl?: Maybe<Scalars['String']>;
assetLimitPerUpdateGroup: Scalars['Int'];
branchesPaginated: AppBranchesConnection;
buildJobs: Array<BuildJob>;
/**
* Coalesced Build (EAS) or BuildJob (Classic) items for this app.
@@ -931,10 +957,8 @@ export type App = Project & {
/** Classic update release channel names that have at least one build */
buildsReleaseChannels: Array<Scalars['String']>;
deployment?: Maybe<Deployment>;
deploymentNew?: Maybe<DeploymentNew>;
/** Deployments associated with this app */
deployments: Array<Deployment>;
deploymentsNew: DeploymentsConnection;
deployments: DeploymentsConnection;
description: Scalars['String'];
/** Environment secrets for an app */
environmentSecrets: Array<EnvironmentSecret>;
@@ -987,6 +1011,8 @@ export type App = Project & {
slug: Scalars['String'];
/** EAS Submissions associated with this app */
submissions: Array<Submission>;
/** Coalesced project activity for an app using pagination */
timelineActivity: TimelineActivityConnection;
/** @deprecated 'likes' have been deprecated. */
trendScore: Scalars['Float'];
/** get an EAS branch owned by the app by name */
@@ -1003,6 +1029,7 @@ export type App = Project & {
updated: Scalars['DateTime'];
/** EAS updates owned by an app */
updates: Array<Update>;
updatesPaginated: AppUpdatesConnection;
/** @deprecated Use ownerAccount.name instead */
username: Scalars['String'];
/** @deprecated No longer supported */
@@ -1029,6 +1056,15 @@ export type AppAndroidAppCredentialsArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppBranchesPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppBuildJobsArgs = {
limit: Scalars['Int'];
@@ -1056,14 +1092,6 @@ export type AppBuildsArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentArgs = {
channel: Scalars['String'];
options?: InputMaybe<DeploymentOptions>;
runtimeVersion: Scalars['String'];
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentNewArgs = {
channel: Scalars['String'];
runtimeVersion: Scalars['String'];
};
@@ -1071,14 +1099,6 @@ export type AppDeploymentNewArgs = {
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsArgs = {
limit: Scalars['Int'];
mostRecentlyUpdatedAt?: InputMaybe<Scalars['DateTime']>;
options?: InputMaybe<DeploymentOptions>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppDeploymentsNewArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
@@ -1127,6 +1147,16 @@ export type AppSubmissionsArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppTimelineActivityArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
filter?: InputMaybe<TimelineActivityFilterInput>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppUpdateBranchByNameArgs = {
name: Scalars['String'];
@@ -1168,11 +1198,32 @@ export type AppUpdatesArgs = {
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppUpdatesPaginatedArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
/** Represents an Exponent App (or Experience in legacy terms) */
export type AppWebhooksArgs = {
filter?: InputMaybe<WebhookFilter>;
};
export type AppBranchEdge = {
__typename?: 'AppBranchEdge';
cursor: Scalars['String'];
node: UpdateBranch;
};
export type AppBranchesConnection = {
__typename?: 'AppBranchesConnection';
edges: Array<AppBranchEdge>;
pageInfo: PageInfo;
};
export type AppDataInput = {
id: Scalars['ID'];
privacy?: InputMaybe<Scalars['String']>;
@@ -1362,6 +1413,18 @@ export enum AppStoreConnectUserRole {
Unknown = 'UNKNOWN'
}
export type AppUpdateEdge = {
__typename?: 'AppUpdateEdge';
cursor: Scalars['String'];
node: Update;
};
export type AppUpdatesConnection = {
__typename?: 'AppUpdatesConnection';
edges: Array<AppUpdateEdge>;
pageInfo: PageInfo;
};
/** Represents Play Store/App Store version of an application */
export type AppVersion = {
__typename?: 'AppVersion';
@@ -1782,7 +1845,10 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'Build';
activityTimestamp: Scalars['DateTime'];
actor?: Maybe<Actor>;
/** The actual resource class of the builder assigned to the build job */
/**
* The actual resource class of the builder assigned to the build job
* @deprecated Use resourceClassDisplayName instead
*/
actualResourceClass?: Maybe<BuildResourceClass>;
appBuildVersion?: Maybe<Scalars['String']>;
appVersion?: Maybe<Scalars['String']>;
@@ -1825,8 +1891,13 @@ export type Build = ActivityTimelineProjectActivity & BuildOrBuildJob & {
queuePosition?: Maybe<Scalars['Int']>;
reactNativeVersion?: Maybe<Scalars['String']>;
releaseChannel?: Maybe<Scalars['String']>;
/** The builder resource class requested by the developer */
/**
* The builder resource class requested by the developer
* @deprecated Use resourceClassDisplayName instead
*/
resourceClass: BuildResourceClass;
/** String describing the resource class used to run the build */
resourceClassDisplayName: Scalars['String'];
runFromCI?: Maybe<Scalars['Boolean']>;
runtimeVersion?: Maybe<Scalars['String']>;
sdkVersion?: Maybe<Scalars['String']>;
@@ -1898,6 +1969,7 @@ export enum BuildIosEnterpriseProvisioning {
/** Represents an Standalone App build job */
export type BuildJob = ActivityTimelineProjectActivity & BuildOrBuildJob & {
__typename?: 'BuildJob';
accountUserActor?: Maybe<UserActor>;
activityTimestamp: Scalars['DateTime'];
actor?: Maybe<Actor>;
app?: Maybe<App>;
@@ -2005,6 +2077,7 @@ export type BuildMetrics = {
export enum BuildMode {
Build = 'BUILD',
Custom = 'CUSTOM',
Resign = 'RESIGN'
}
@@ -2181,12 +2254,18 @@ export enum BuildResourceClass {
AndroidLarge = 'ANDROID_LARGE',
AndroidMedium = 'ANDROID_MEDIUM',
IosDefault = 'IOS_DEFAULT',
/** @deprecated Use IOS_INTEL_MEDIUM instead */
IosIntelLarge = 'IOS_INTEL_LARGE',
IosIntelMedium = 'IOS_INTEL_MEDIUM',
IosLarge = 'IOS_LARGE',
/** @deprecated Use IOS_M_MEDIUM instead */
IosM1Large = 'IOS_M1_LARGE',
IosM1Medium = 'IOS_M1_MEDIUM',
IosM2Medium = 'IOS_M2_MEDIUM',
IosM2ProMedium = 'IOS_M2_PRO_MEDIUM',
IosMedium = 'IOS_MEDIUM',
IosMLarge = 'IOS_M_LARGE',
IosMMedium = 'IOS_M_MEDIUM',
Legacy = 'LEGACY'
}
@@ -2322,7 +2401,7 @@ export type CreateGitHubRepositoryInput = {
export type CreateGitHubRepositorySettingsInput = {
appId: Scalars['ID'];
/** The base directory is the directory to change to before starting a build. This string should be a properly formatted Unix path starting with '/', './', or the name of the directory relative to the root of the repository. Valid examples include: '/apps/expo-app', './apps/expo-app', and 'apps/expo-app'. This is intended for monorepos or apps that live in a subdirectory of a repository. */
/** The base directory is the directory to change to before starting a build. This string should be a properly formatted POSIX path starting with '/', './', or the name of the directory relative to the root of the repository. Valid examples include: '/apps/expo-app', './apps/expo-app', and 'apps/expo-app'. This is intended for monorepos or apps that live in a subdirectory of a repository. */
baseDirectory: Scalars['String'];
};
@@ -2346,6 +2425,10 @@ export type CreateSubmissionResult = {
submission: Submission;
};
export type CustomBuildConfigInput = {
path: Scalars['String'];
};
export type DeleteAccessTokenResult = {
__typename?: 'DeleteAccessTokenResult';
id: Scalars['ID'];
@@ -2434,16 +2517,19 @@ export type DeployServerlessFunctionResult = {
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type Deployment = {
__typename?: 'Deployment';
channel?: Maybe<UpdateChannel>;
/**
* The name of this deployment's associated channel. It is specified separately from the `channel`
* field to allow specifying a deployment before an EAS Update channel has been created.
*/
channelName: Scalars['String'];
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
mostRecentlyUpdatedAt: Scalars['DateTime'];
recentBuilds: Array<Build>;
runtimeVersion: Scalars['String'];
runtime: Runtime;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentBuildsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type DeploymentBuildEdge = {
@@ -2462,33 +2548,10 @@ export type DeploymentBuildsConnection = {
export type DeploymentEdge = {
__typename?: 'DeploymentEdge';
cursor: Scalars['String'];
node: DeploymentNew;
node: Deployment;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNew = {
__typename?: 'DeploymentNew';
builds: DeploymentBuildsConnection;
channel: UpdateChannel;
id: Scalars['ID'];
runtime: Runtime;
};
/** Represents a Deployment - a set of Builds with the same Runtime Version and Channel */
export type DeploymentNewBuildsArgs = {
after?: InputMaybe<Scalars['String']>;
before?: InputMaybe<Scalars['String']>;
first?: InputMaybe<Scalars['Int']>;
last?: InputMaybe<Scalars['Int']>;
};
export type DeploymentOptions = {
/** Max number of associated builds to return */
buildListMaxSize?: InputMaybe<Scalars['Int']>;
};
/** Represents the connection over the deploymentsNew edge of an App */
/** Represents the connection over the deployments edge of an App */
export type DeploymentsConnection = {
__typename?: 'DeploymentsConnection';
edges: Array<DeploymentEdge>;
@@ -2514,6 +2577,11 @@ export enum EasBuildDeprecationInfoType {
export type EasBuildOrClassicBuildJob = Build | BuildJob;
export enum EasService {
Builds = 'BUILDS',
Updates = 'UPDATES'
}
export enum EasServiceMetric {
AssetsRequests = 'ASSETS_REQUESTS',
BandwidthUsage = 'BANDWIDTH_USAGE',
@@ -3103,6 +3171,7 @@ export type IosJobInput = {
buildType?: InputMaybe<IosBuildType>;
builderEnvironment?: InputMaybe<IosBuilderEnvironmentInput>;
cache?: InputMaybe<BuildCacheInput>;
customBuildConfig?: InputMaybe<CustomBuildConfigInput>;
developmentClient?: InputMaybe<Scalars['Boolean']>;
/** @deprecated */
distribution?: InputMaybe<DistributionType>;
@@ -3794,6 +3863,8 @@ export type RootQuery = {
updatesByGroup: Array<Update>;
/** Top-level query object for querying Users. */
user: UserQuery;
/** Top-level query object for querying UserActors. */
userActor: UserActorQuery;
/** @deprecated Use 'byId' field under 'user'. */
userByUserId?: Maybe<User>;
/** @deprecated Use 'byUsername' field under 'user'. */
@@ -3857,6 +3928,7 @@ export type SsoUser = Actor & UserActor & {
appetizeCode?: Maybe<Scalars['String']>;
/** Apps this user has published. If this user is the viewer, this field returns the apps the user has access to. */
apps: Array<App>;
bestContactEmail?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
displayName: Scalars['String'];
/**
@@ -3872,6 +3944,7 @@ export type SsoUser = Actor & UserActor & {
isExpoAdmin: Scalars['Boolean'];
lastName?: Maybe<Scalars['String']>;
location?: Maybe<Scalars['String']>;
notificationSubscriptions: Array<NotificationSubscription>;
/** Associated accounts */
primaryAccount: Account;
profilePhoto: Scalars['String'];
@@ -3904,6 +3977,12 @@ export type SsoUserFeatureGatesArgs = {
};
/** Represents a human SSO (not robot) actor. */
export type SsoUserNotificationSubscriptionsArgs = {
filter?: InputMaybe<NotificationSubscriptionFilter>;
};
/** Represents a human SSO (not robot) actor. */
export type SsoUserSnacksArgs = {
limit: Scalars['Int'];
@@ -4304,6 +4383,25 @@ export type SubscriptionDetailsPlanEnablementArgs = {
serviceMetric: EasServiceMetric;
};
export type TimelineActivityConnection = {
__typename?: 'TimelineActivityConnection';
edges: Array<TimelineActivityEdge>;
pageInfo: PageInfo;
};
export type TimelineActivityEdge = {
__typename?: 'TimelineActivityEdge';
cursor: Scalars['String'];
node: ActivityTimelineProjectActivity;
};
export type TimelineActivityFilterInput = {
channels?: InputMaybe<Array<Scalars['String']>>;
platforms?: InputMaybe<Array<AppPlatform>>;
releaseChannels?: InputMaybe<Array<Scalars['String']>>;
types?: InputMaybe<Array<ActivityTimelineProjectActivityType>>;
};
export type UnsubscribeFromNotificationResult = {
__typename?: 'UnsubscribeFromNotificationResult';
notificationSubscription: NotificationSubscription;
@@ -4542,6 +4640,7 @@ export type User = Actor & UserActor & {
appetizeCode?: Maybe<Scalars['String']>;
/** Apps this user has published */
apps: Array<App>;
bestContactEmail?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
displayName: Scalars['String'];
email?: Maybe<Scalars['String']>;
@@ -4559,6 +4658,8 @@ export type User = Actor & UserActor & {
id: Scalars['ID'];
industry?: Maybe<Scalars['String']>;
isExpoAdmin: Scalars['Boolean'];
/** @deprecated This field no longer exists */
isLegacy: Scalars['Boolean'];
isSecondFactorAuthenticationEnabled: Scalars['Boolean'];
lastName?: Maybe<Scalars['String']>;
location?: Maybe<Scalars['String']>;
@@ -4625,6 +4726,7 @@ export type UserActor = {
appetizeCode?: Maybe<Scalars['String']>;
/** Apps this user has published */
apps: Array<App>;
bestContactEmail?: Maybe<Scalars['String']>;
created: Scalars['DateTime'];
/**
* Best-effort human readable name for this human actor for use in user interfaces during action attribution.
@@ -4644,6 +4746,7 @@ export type UserActor = {
isExpoAdmin: Scalars['Boolean'];
lastName?: Maybe<Scalars['String']>;
location?: Maybe<Scalars['String']>;
notificationSubscriptions: Array<NotificationSubscription>;
/** Associated accounts */
primaryAccount: Account;
profilePhoto: Scalars['String'];
@@ -4676,12 +4779,36 @@ export type UserActorFeatureGatesArgs = {
};
/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */
export type UserActorNotificationSubscriptionsArgs = {
filter?: InputMaybe<NotificationSubscriptionFilter>;
};
/** A human user (type User or SSOUser) that can login to the Expo website, use Expo services, and be a member of accounts. */
export type UserActorSnacksArgs = {
limit: Scalars['Int'];
offset: Scalars['Int'];
};
export type UserActorQuery = {
__typename?: 'UserActorQuery';
/** Query a UserActor by ID */
byId: UserActor;
/** Query a UserActor by username */
byUsername: UserActor;
};
export type UserActorQueryByIdArgs = {
id: Scalars['ID'];
};
export type UserActorQueryByUsernameArgs = {
username: Scalars['String'];
};
export type UserDataInput = {
appetizeCode?: InputMaybe<Scalars['String']>;
email?: InputMaybe<Scalars['String']>;
@@ -4805,6 +4932,7 @@ export type UserPermission = {
role?: Maybe<Role>;
/** @deprecated User type is deprecated */
user?: Maybe<User>;
userActor?: Maybe<UserActor>;
};
export type UserQuery = {
@@ -5393,7 +5521,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
}>;
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutationVariables = Exact<{
appId: Scalars['ID'];
@@ -5403,7 +5531,7 @@ export type CreateIosBuildMutationVariables = Exact<{
}>;
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
export type UpdateBuildMetadataMutationVariables = Exact<{
buildId: Scalars['ID'];
@@ -5411,7 +5539,7 @@ export type UpdateBuildMetadataMutationVariables = Exact<{
}>;
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type RetryIosBuildMutationVariables = Exact<{
buildId: Scalars['ID'];
@@ -5419,7 +5547,7 @@ export type RetryIosBuildMutationVariables = Exact<{
}>;
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
input: CreateEnvironmentSecretInput;
@@ -5576,14 +5704,14 @@ export type BuildsByIdQueryVariables = Exact<{
}>;
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
buildId: Scalars['ID'];
}>;
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } } } };
export type ViewBuildsOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5593,7 +5721,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
}>;
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } }> } } };
export type ViewUpdateChannelOnAppQueryVariables = Exact<{
appId: Scalars['String'];
@@ -5710,9 +5838,9 @@ export type AccountFragment = { __typename?: 'Account', id: string, name: string
export type AppFragment = { __typename?: 'App', id: string, fullName: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string, users: Array<{ __typename?: 'UserPermission', role?: Role | null, actor: { __typename?: 'Robot', id: string } | { __typename?: 'SSOUser', id: string } | { __typename?: 'User', id: string } }> } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, completedAt?: any | null, resourceClass: BuildResourceClass, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, releaseChannel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, resourceClass: BuildResourceClass, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logsUrl?: string | null, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: SubmissionAndroidTrack, releaseStatus?: SubmissionAndroidReleaseStatus | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string } };
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
@@ -52,6 +52,7 @@ export const BuildFragmentNode = gql`
priority
createdAt
updatedAt
message
completedAt
resourceClass
}
+3 -2
View File
@@ -90,11 +90,12 @@ export default class Log {
*/
export function link(
url: string,
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
const output = terminalLink(text, url, {
fallback: () => (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
fallback: () =>
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
@@ -16,7 +16,9 @@ import {
convertAssetToUpdateInfoGroupFormatAsync,
filterExportedPlatformsByFlag,
filterOutAssetsThatAlreadyExistAsync,
getAssetHashFromPath,
getBase64URLEncoding,
getOriginalPathFromAssetMap,
getStorageKey,
getStorageKeyForAssetAsync,
guessContentTypeFromExtension,
@@ -279,9 +281,55 @@ describe(resolveInputDirectoryAsync, () => {
});
});
describe(getAssetHashFromPath, () => {
it('returns asset hash from path', () => {
expect(getAssetHashFromPath('assets/5b2a819c71d035ca45d223e4c47ed4f9')).toBe(
'5b2a819c71d035ca45d223e4c47ed4f9'
);
});
it('returns null for incorrect path', () => {
expect(getAssetHashFromPath('assets/this/is/not/a/hash.jpg')).toBeNull();
});
});
describe(getOriginalPathFromAssetMap, () => {
// Partial assetmap.json, with only fields we need
const fakeAssetMap = {
'5b2a819c71d035ca45d223e4c47ed4f9': {
httpServerLocation: '/assets/src/assets',
name: 'asset-420',
type: 'jpg',
},
};
it('returns null path when asset map is null', () => {
expect(
getOriginalPathFromAssetMap(null, {
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
ext: 'jpg',
})
).toBeNull();
});
it('returns null when asset is not found in asset map', () => {
expect(
getOriginalPathFromAssetMap(fakeAssetMap, {
path: 'assets/fb64d3b2fb71b3d739ad5c13a93e12c5',
ext: 'jpg',
})
).toBeNull();
});
it('returns reconstructed original path from existing asset in asset map', () => {
expect(
getOriginalPathFromAssetMap(fakeAssetMap, {
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
ext: 'jpg',
})
).toBe('/src/assets/asset-420.jpg');
});
});
describe(collectAssetsAsync, () => {
it('builds an update info group', async () => {
const fakeHash = 'md5-hash-of-jpg';
const fakeHash = 'hdahukw8adhawi8fawhfa8';
const bundles = {
android: 'android-bundle-code',
ios: 'ios-bundle-code',
@@ -294,6 +342,7 @@ describe(collectAssetsAsync, () => {
fileExtension: '.jpg',
contentType: 'image/jpeg',
path: `${inputDir}/assets/${fakeHash}`,
originalPath: `assets/wat.jpg`,
},
];
@@ -326,6 +375,24 @@ describe(collectAssetsAsync, () => {
},
})
);
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],
},
})
);
expect(await collectAssetsAsync(inputDir)).toEqual({
android: {
@@ -410,6 +477,7 @@ describe(uploadAssetsAsync, () => {
const androidBundlePath = uuidv4();
const iosBundlePath = uuidv4();
const dummyFilePath = uuidv4();
const dummyOriginalFilePath = uuidv4();
const userDefinedPath = uuidv4();
const testProjectId = uuidv4();
const expectedAssetLimit = 1400;
@@ -427,7 +495,15 @@ describe(uploadAssetsAsync, () => {
contentType: 'application/javascript',
path: androidBundlePath,
},
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
ios: {
launchAsset: {
@@ -435,7 +511,15 @@ describe(uploadAssetsAsync, () => {
contentType: 'application/javascript',
path: androidBundlePath,
},
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
assets: [
userDefinedAsset,
{
type: 'jpg',
contentType: 'image/jpeg',
path: dummyFilePath,
originalPath: dummyOriginalFilePath,
},
],
},
};
@@ -490,8 +574,10 @@ describe(uploadAssetsAsync, () => {
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
).resolves.toEqual({
assetCount: 6,
launchAssetCount: 2,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 0,
uniqueUploadedAssetPaths: [],
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
@@ -526,8 +612,10 @@ describe(uploadAssetsAsync, () => {
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
).resolves.toEqual({
assetCount: 6,
launchAssetCount: 2,
uniqueAssetCount: 3,
uniqueUploadedAssetCount: 2,
uniqueUploadedAssetPaths: [],
assetLimitPerUpdateGroup: expectedAssetLimit,
});
});
@@ -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)(.*)(Release|Debug)/);
const matchResult = taskName.match(/(build|bundle|assemble|package)(.*)(Release|Debug)/);
if (!matchResult) {
throw new Error(`Failed to parse gradle command: ${cmd}`);
}
@@ -0,0 +1,26 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
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 relativeBuildConfigPath = getCustomBuildConfigPath(profile.config);
const buildConfigPath = path.join(projectDir, relativeBuildConfigPath);
if (!(await fs.pathExists(buildConfigPath))) {
throw new Error(
`Custom build configuration file ${chalk.bold(relativeBuildConfigPath)} does not exist.`
);
}
}
export function getCustomBuildConfigPath(configFilename: string): string {
return path.join('.eas/build', configFilename);
}
+68
View File
@@ -15,6 +15,7 @@ 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 uniqBy from '../utils/expodash/uniqBy';
export type ExpoCLIExportPlatformFlag = Platform | 'all';
@@ -30,6 +31,8 @@ export type RawAsset = {
fileExtension?: string;
contentType: string;
path: string;
/** Original asset path derrived from asset map, or exported folder */
originalPath?: string;
};
type CollectedAssets = {
@@ -52,6 +55,16 @@ type UpdateInfoGroup = {
[key in Platform]: ManifestFragment;
};
// Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes`
type AssetMap = Record<
string,
{
httpServerLocation: string;
name: string;
type: string;
}
>;
const fileMetadataJoi = Joi.object({
assets: Joi.array()
.required()
@@ -170,6 +183,7 @@ export async function buildBundlesAsync({
'--output-dir',
inputDir,
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
]);
@@ -182,6 +196,7 @@ export async function buildBundlesAsync({
'--experimental-bundle',
'--non-interactive',
'--dump-sourcemap',
'--dump-assetmap',
'--platform',
platformFlag,
]);
@@ -246,9 +261,48 @@ export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform,
return { [platform]: record[platform] } as T;
}
/** Try to load the asset map for logging the names of assets published */
export async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
const assetMapPath = path.join(distRoot, 'assetmap.json');
if (!(await fs.pathExists(assetMapPath))) {
return null;
}
const assetMap: AssetMap = JsonFile.read(path.join(distRoot, 'assetmap.json'));
// TODO: basic validation?
return assetMap;
}
// exposed for testing
export function getAssetHashFromPath(assetPath: string): string | null {
const [, hash] = assetPath.match(new RegExp(/assets\/([a-z0-9]+)$/, 'i')) ?? [];
return hash ?? null;
}
// exposed for testing
export function getOriginalPathFromAssetMap(
assetMap: AssetMap | null,
asset: { path: string; ext: string }
): string | null {
if (!assetMap) {
return null;
}
const assetHash = getAssetHashFromPath(asset.path);
const assetMapEntry = assetHash && assetMap[assetHash];
if (!assetMapEntry) {
return null;
}
const pathPrefix = assetMapEntry.httpServerLocation.substring('/assets'.length);
return `${pathPrefix}/${assetMapEntry.name}.${assetMapEntry.type}`;
}
/** 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);
const collectedAssets: CollectedAssets = {};
@@ -261,6 +315,7 @@ export async function collectAssetsAsync(dir: string): Promise<CollectedAssets>
},
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),
})),
@@ -294,9 +349,17 @@ export async function filterOutAssetsThatAlreadyExistAsync(
}
type AssetUploadResult = {
/** All found assets within the exported folder per platform */
assetCount: number;
/** The uploaded JS bundles, per platform */
launchAssetCount: number;
/** All unique assets within the exported folder with platforms combined */
uniqueAssetCount: number;
/** All unique assets uploaded */
uniqueUploadedAssetCount: number;
/** All (non-launch) asset original paths, used for logging */
uniqueUploadedAssetPaths: string[];
/** The asset limit received from the server */
assetLimitPerUpdateGroup: number;
};
@@ -308,7 +371,9 @@ export async function uploadAssetsAsync(
): 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,
@@ -335,6 +400,7 @@ export async function uploadAssetsAsync(
updateSpinnerText?.(totalAssets, totalAssets);
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(graphqlClient, uniqueAssets);
const uniqueUploadedAssetCount = missingAssets.length;
const uniqueUploadedAssetPaths = missingAssets.map(asset => asset.originalPath).filter(truthy);
const missingAssetChunks = chunk(missingAssets, 100);
const specifications: string[] = [];
@@ -372,8 +438,10 @@ export async function uploadAssetsAsync(
}
return {
assetCount: assets.length,
launchAssetCount: launchAssets.length,
uniqueAssetCount: uniqueAssets.length,
uniqueUploadedAssetCount,
uniqueUploadedAssetPaths,
assetLimitPerUpdateGroup,
};
}
+33 -15
View File
@@ -317,30 +317,48 @@ async function handleBuildListSourceAsync(
}
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
build;
const formatValue = (field?: string | null): string => (field ? chalk.bold(field) : 'Unknown');
const {
id,
updatedAt,
runtimeVersion,
buildProfile,
gitCommitHash,
gitCommitMessage,
channel,
message,
} = build;
const buildDate = new Date(updatedAt);
const splitCommitMessage = gitCommitMessage?.split('\n');
const formattedCommitData =
gitCommitHash && gitCommitMessage
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(gitCommitMessage)}"`
: 'Unknown';
gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
)}"`
: null;
const title = `${chalk.bold(`ID:`)} ${id} (${chalk.bold(`${fromNow(buildDate)} ago`)})`;
const description = [
`\t${chalk.bold(`Profile:`)} ${formatValue(buildProfile)}`,
`\t${chalk.bold(`Channel:`)} ${formatValue(channel)}`,
`\t${chalk.bold(`Runtime version:`)} ${formatValue(runtimeVersion)}`,
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
].join('\n');
const descriptionItems: { name: string; value: string | null }[] = [
{ name: 'Profile', value: buildProfile ? chalk.bold(buildProfile) : null },
{ name: 'Channel', value: channel ? chalk.bold(channel) : null },
{ name: 'Runtime version', value: runtimeVersion ? chalk.bold(runtimeVersion) : null },
{ name: 'Commit', value: formattedCommitData },
{
name: 'Message',
value: message
? chalk.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
: null,
},
];
const filteredDescriptionArray: string[] = descriptionItems
.filter(item => item.value)
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
return {
title,
description,
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
value: build,
disabled: buildDate < expiryDate,
};
+4 -3
View File
@@ -1,12 +1,12 @@
{
"name": "@expo/eas-json",
"description": "A library for interacting with eas.json",
"version": "3.5.0",
"version": "3.8.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.18.6",
"@expo/eas-build-job": "0.2.102",
"@expo/eas-build-job": "0.2.106",
"chalk": "4.1.2",
"env-string": "1.0.1",
"fs-extra": "10.1.0",
@@ -36,7 +36,8 @@
"build": "tsc --project tsconfig.build.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "rimraf build && yarn build",
"prepack": "yarn rebuild",
"rebuild": "rimraf build && yarn build",
"test": "jest",
"clean": "rimraf build node_modules yarn-error.log"
},
+4 -3
View File
@@ -329,6 +329,7 @@
"enum": [
"default",
"latest",
"macos-monterey-12.6-xcode-14.2",
"macos-monterey-12.6-xcode-14.1",
"macos-monterey-12.6-xcode-14.0",
"macos-monterey-12.4-xcode-13.4",
@@ -338,8 +339,8 @@
"macos-big-sur-11.4-xcode-12.5"
],
"markdownEnumDescriptions": [
"`macos-monterey-12.6-xcode-14.0`",
"`macos-monterey-12.6-xcode-14.1`"
"`macos-monterey-12.6-xcode-14.1`",
"`macos-monterey-12.6-xcode-14.2`"
]
},
{
@@ -353,7 +354,7 @@
"default": "default",
"anyOf": [
{
"enum": ["default", "medium", "m1-medium", "intel-medium"]
"enum": ["default", "medium", "intel-medium", "m-medium"]
},
{
"type": "string"
@@ -399,3 +399,64 @@ test('Android-specific resourceClass', async () => {
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 = new EasJsonAccessor('/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 = new EasJsonAccessor('/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',
});
});
+69 -38
View File
@@ -14,6 +14,8 @@ const AllowedIosResourceClasses: ResourceClass[] = [
...AllowedCommonResourceClasses,
ResourceClass.M1_MEDIUM,
ResourceClass.INTEL_MEDIUM,
ResourceClass.M_MEDIUM,
ResourceClass.M_LARGE,
];
const CacheSchema = Joi.object({
@@ -24,69 +26,98 @@ const CacheSchema = Joi.object({
});
const CommonBuildProfileSchema = Joi.object({
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()),
// builder
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
// 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),
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
// 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
autoIncrement: Joi.alternatives().try(Joi.boolean()),
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
// artifacts
buildArtifactPaths: Joi.array().items(Joi.string()),
// cache
cache: CacheSchema,
// custom build configuration
config: Joi.string(),
});
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
// 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
withoutCredentials: Joi.boolean(),
image: Joi.string(),
ndk: Joi.string().empty(null).custom(semverCheck),
// build configuration
gradleCommand: Joi.string(),
buildType: Joi.string().valid('apk', 'app-bundle'),
// versions
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 = CommonBuildProfileSchema.concat(
const IosBuildProfileSchema = PlatformBuildProfileSchema.concat(
Joi.object({
credentialsSource: Joi.string().valid('local', 'remote'),
distribution: Joi.string().valid('store', 'internal'),
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'buildNumber')
),
simulator: Joi.boolean(),
// builder
resourceClass: Joi.string().valid(...AllowedIosResourceClasses),
image: Joi.string(),
// build environment
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()),
// credentials
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
// build configuration
simulator: Joi.boolean(),
scheme: Joi.string(),
buildConfiguration: Joi.string(),
}).oxor('artifactPath', 'applicationArchivePath')
// versions
autoIncrement: Joi.alternatives().try(
Joi.boolean(),
Joi.string().valid('version', 'buildNumber')
),
})
);
export const BuildProfileSchema = CommonBuildProfileSchema.concat(
+64 -31
View File
@@ -9,15 +9,21 @@ export enum ResourceClass {
DEFAULT = 'default',
LARGE = 'large',
/**
* @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.
* @deprecated use M_MEDIUM instead
*/
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';
@@ -29,32 +35,42 @@ export type IosVersionAutoIncrement = VersionAutoIncrement | 'buildNumber';
export type AndroidVersionAutoIncrement = VersionAutoIncrement | 'versionCode';
export interface CommonBuildProfile {
credentialsSource: CredentialsSource;
distribution: DistributionType;
cache?: Omit<Cache, 'clear'>;
releaseChannel?: string;
channel?: string;
developmentClient?: boolean;
prebuildCommand?: string;
autoIncrement?: boolean;
// builder
resourceClass?: ResourceClass;
buildArtifactPaths?: string[];
// build environment
env?: Record<string, string>;
node?: string;
yarn?: string;
expoCli?: string;
env?: Record<string, 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;
}
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;
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
// artifacts
/**
* @deprecated use applicationArchivePath
*/
@@ -62,22 +78,39 @@ export interface AndroidBuildProfile extends Omit<CommonBuildProfile, 'autoIncre
applicationArchivePath?: string;
}
export interface IosBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
enterpriseProvisioning?: IosEnterpriseProvisioning;
autoIncrement?: IosVersionAutoIncrement;
simulator?: boolean;
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
image?: Ios.BuilderEnvironment['image'];
bundler?: string;
fastlane?: string;
cocoapods?: string;
/**
* @deprecated use applicationArchivePath
*/
artifactPath?: string;
applicationArchivePath?: string;
// credentials
enterpriseProvisioning?: IosEnterpriseProvisioning;
// build configuration
simulator?: boolean;
scheme?: string;
buildConfiguration?: string;
// versions
autoIncrement?: IosVersionAutoIncrement;
}
export type BuildProfile<TPlatform extends Platform = Platform> = TPlatform extends Platform.ANDROID
Generated
+24 -44
View File
@@ -1324,10 +1324,10 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@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/apple-utils@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-1.1.0.tgz#62cd724436b8eaa9cafcc3648ea211af8409fc71"
integrity sha512-caw0o9HeQHX7xeXppkXI3B/PmPyVOkEjWgXdv0dYzgW/rwvZRLwtMNFXX5t+dJy2hd2UpZlrixaoYyvxuXPtbQ==
"@expo/bunyan@^4.0.0":
version "4.0.0"
@@ -1390,21 +1390,13 @@
slugify "^1.3.4"
sucrase "^3.20.0"
"@expo/[email protected]2":
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==
"@expo/[email protected]6":
version "0.2.106"
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.106.tgz#a10f791986bfa9777a13b2b0e9c820673e9e38ba"
integrity sha512-V7ihSFNFaATM31P1b/tGrGG7sjuXBT5ZAzKnybTWr5Xl1ODOdEpSC1c9bzs9SUr2iWUFVsxyd2/7+f2+7r/l4A==
dependencies:
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"
joi "^17.7.0"
semver "^7.3.8"
"@expo/[email protected]":
version "0.3.22"
@@ -3985,13 +3977,6 @@
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"
@@ -3999,10 +3984,10 @@
dependencies:
"@hapi/hoek" "^9.0.0"
"@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/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/pinpoint@^2.0.0":
version "2.0.0"
@@ -9328,15 +9313,15 @@ [email protected]:
"@sideway/formula" "^3.0.0"
"@sideway/pinpoint" "^2.0.0"
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==
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==
dependencies:
"@hapi/hoek" "^9.0.0"
"@hapi/topo" "^5.0.0"
"@sideway/address" "^4.1.0"
"@sideway/formula" "^3.0.0"
"@sideway/address" "^4.1.3"
"@sideway/formula" "^3.0.1"
"@sideway/pinpoint" "^2.0.0"
join-component@^1.1.0:
@@ -13190,18 +13175,13 @@ unc-path-regex@^0.1.2:
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo=
undici@^5.12.0:
version "5.14.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.14.0.tgz#1169d0cdee06a4ffdd30810f6228d57998884d00"
integrity sha512-yJlHYw6yXPPsuOH0x2Ib1Km61vu4hLiRRQoafs+WUgX1vO64vgnxiCEN9dpIrhZyHFsai3F0AEj4P9zy19enEQ==
undici@^5.12.0, undici@^5.8.0:
version "5.19.1"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.19.1.tgz#92b1fd3ab2c089b5a6bd3e579dcda8f1934ebf6d"
integrity sha512-YiZ61LPIgY73E7syxCDxxa3LV2yl3sN8spnIuTct60boiiRaE1J8mNWHO8Im2Zi/sFrPusjLlmRPrsyraSqX6A==
dependencies:
busboy "^1.6.0"
undici@^5.8.0:
version "5.8.2"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.8.2.tgz#071fc8a6a5d24db0ad510ad442f607d9b09d5eec"
integrity sha512-3KLq3pXMS0Y4IELV045fTxqz04Nk9Ms7yfBBHum3yxsTR4XNn+ZCaUbf/mWitgYDAhsplQ0B1G4S5D345lMO3A==
unique-filename@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"