mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
51
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bcd94e83f | ||
|
|
da4b56a58d | ||
|
|
1080600af6 | ||
|
|
dcb6713f12 | ||
|
|
24a1d2d581 | ||
|
|
8af88c2d29 | ||
|
|
d416ac2162 | ||
|
|
7bf0493a4d | ||
|
|
91f32cc377 | ||
|
|
cf54f62bac | ||
|
|
3ef3e8447c | ||
|
|
ad64e1afad | ||
|
|
a0800cf7ef | ||
|
|
34436ee674 | ||
|
|
1072b75927 | ||
|
|
c0c9d2e7df | ||
|
|
19b831667f | ||
|
|
09db7da23b | ||
|
|
f08dde2469 | ||
|
|
213b5d240c | ||
|
|
16ebdce913 | ||
|
|
121ff7a718 | ||
|
|
360393f647 | ||
|
|
82d66054dc | ||
|
|
d8849dbf8c | ||
|
|
4d6b5f435b | ||
|
|
8fe7dfea29 | ||
|
|
8750155c25 | ||
|
|
a0fce1c773 | ||
|
|
cad3dca5df | ||
|
|
2ef6e79395 | ||
|
|
44270814a7 | ||
|
|
95155681db | ||
|
|
53fd599d0a | ||
|
|
b51201d010 | ||
|
|
f86c079733 | ||
|
|
843ec7407b | ||
|
|
537cd7872a | ||
|
|
007c40bed4 | ||
|
|
ba9f8d8ddd | ||
|
|
e4bd933e81 | ||
|
|
a6434bf6b8 | ||
|
|
19bad84109 | ||
|
|
5d9f8f9d76 | ||
|
|
d9d884d211 | ||
|
|
f902bb845b | ||
|
|
03c76c1e0e | ||
|
|
1d0d329cdb | ||
|
|
7ed1efb7d6 | ||
|
|
b7bfec0366 | ||
|
|
a5308f0d37 |
No files matched your search
@@ -37,7 +37,7 @@ body:
|
||||
required: true
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
|
||||
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: false
|
||||
|
||||
@@ -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==
|
||||
@@ -199,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: ''
|
||||
@@ -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 }}
|
||||
+79
-2
@@ -8,12 +8,89 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for EAS Update asset excludes. ([#1526](https://github.com/expo/eas-cli/pull/1526) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [3.9.3](https://github.com/expo/eas-cli/releases/tag/v3.9.3) - 2023-04-18
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix prompts in `eas update`. ([#1797](https://github.com/expo/eas-cli/pull/1797) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [3.9.2](https://github.com/expo/eas-cli/releases/tag/v3.9.2) - 2023-04-13
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixes the rollout percentages when ending a rollout. ([#1781](https://github.com/expo/eas-cli/pull/1781) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Print meaningful error message if `extra.eas.projectId` is not a string. ([#1788](https://github.com/expo/eas-cli/pull/1788) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Show output of `expo install expo-updates`. ([#1782](https://github.com/expo/eas-cli/pull/1782) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Print deprecation warnings for deprecated `eas.json` fields. ([#1768](https://github.com/expo/eas-cli/pull/1768) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.1](https://github.com/expo/eas-cli/releases/tag/v3.9.1) - 2023-04-10
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update apple utilities to work around the maintenance error. ([#1780](https://github.com/expo/eas-cli/pull/1780) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Don't use defaults for the `cache.cacheDefaultPaths` build profile field. ([#1769](https://github.com/expo/eas-cli/pull/1769) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.0](https://github.com/expo/eas-cli/releases/tag/v3.9.0) - 2023-04-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for roll back to embedded updates. ([#1754](https://github.com/expo/eas-cli/pull/1754), [#1755](https://github.com/expo/eas-cli/pull/1755) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update `expoCommandAsync` to support the new Expo CLI bin path. ([#1772](https://github.com/expo/eas-cli/pull/1772) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Use the `eas-cli` npm tag for checking for the local build plugin updates. ([#1759](https://github.com/expo/eas-cli/pull/1759) by [@dsokal](https://github.com/dsokal))
|
||||
- Ignore `requireCommit` option in `eas build:internal`. ([#1760](https://github.com/expo/eas-cli/pull/1760) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support parsing eas.json from a string. ([#1766](https://github.com/expo/eas-cli/pull/1766) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.8.1](https://github.com/expo/eas-cli/releases/tag/v3.8.1) - 2023-03-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
|
||||
|
||||
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.6.1",
|
||||
"version": "3.9.3",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+79
-54
@@ -79,6 +79,7 @@ eas --help COMMAND
|
||||
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
|
||||
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
|
||||
* [`eas channel:list`](#eas-channellist)
|
||||
* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
|
||||
* [`eas channel:view [NAME]`](#eas-channelview-name)
|
||||
* [`eas config`](#eas-config)
|
||||
* [`eas credentials`](#eas-credentials)
|
||||
@@ -130,7 +131,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -147,7 +148,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -164,7 +165,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -178,7 +179,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -228,7 +229,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -249,7 +250,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -269,7 +270,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -289,7 +290,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -312,7 +313,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -342,7 +343,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -359,7 +360,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -376,7 +377,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -411,7 +412,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -448,7 +449,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -474,7 +475,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -498,7 +499,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -545,7 +546,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -564,7 +565,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -581,7 +582,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -602,7 +603,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -624,7 +625,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -644,7 +645,31 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
Roll a new branch out on a channel incrementally.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas channel:rollout [CHANNEL] [--branch <value>] [--percent <value>] [--end] [--json --non-interactive]
|
||||
|
||||
ARGUMENTS
|
||||
CHANNEL channel on which the rollout should be done
|
||||
|
||||
FLAGS
|
||||
--branch=<value> branch to rollout
|
||||
--end end the rollout
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--percent=<value> percent of users to send to the new branch
|
||||
|
||||
DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -667,7 +692,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -688,7 +713,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -705,7 +730,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -719,7 +744,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -739,7 +764,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -760,7 +785,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -774,7 +799,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -788,7 +813,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -876,7 +901,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -893,7 +918,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -910,7 +935,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -924,7 +949,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -938,7 +963,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -960,7 +985,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -983,7 +1008,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -1001,7 +1026,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -1015,7 +1040,7 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas secret:push`
|
||||
|
||||
@@ -1035,7 +1060,7 @@ DESCRIPTION
|
||||
read environment secrets from env file and store on the server
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1065,7 +1090,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1097,7 +1122,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1115,7 +1140,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1136,7 +1161,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1158,11 +1183,11 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
rollback to an existing update
|
||||
roll back to an existing update
|
||||
|
||||
```
|
||||
USAGE
|
||||
@@ -1179,10 +1204,10 @@ FLAGS
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
rollback to an existing update
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1202,7 +1227,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1223,7 +1248,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1243,7 +1268,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1261,7 +1286,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1283,7 +1308,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1300,7 +1325,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.6.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.9.3/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+2796
-680
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.6.1",
|
||||
"version": "3.9.3",
|
||||
"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.2.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.6.1",
|
||||
"@expo/eas-build-job": "1.0.7",
|
||||
"@expo/eas-json": "3.9.2",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
@@ -27,6 +27,7 @@
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/steps": "1.0.8",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.23.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.10",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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';
|
||||
@@ -24,7 +25,6 @@ interface JobData {
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
@@ -49,11 +49,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 +79,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 +93,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);
|
||||
|
||||
@@ -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.'
|
||||
);
|
||||
}
|
||||
@@ -607,7 +612,7 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
}
|
||||
|
||||
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -26,17 +28,17 @@ interface JobData {
|
||||
const cacheDefaults = {
|
||||
disabled: false,
|
||||
customPaths: [],
|
||||
cacheDefaultPaths: true,
|
||||
};
|
||||
|
||||
export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const 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 +46,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 +88,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -98,7 +99,7 @@ export async function runBuildAndSubmitAsync(
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
@@ -118,6 +119,7 @@ export async function runBuildAndSubmitAsync(
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
|
||||
}
|
||||
|
||||
const startedBuilds: {
|
||||
|
||||
@@ -57,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');
|
||||
@@ -68,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]} ${
|
||||
@@ -82,6 +81,7 @@ function printBuildResult(build: BuildFragment): void {
|
||||
return;
|
||||
}
|
||||
if (build.status === BuildStatus.Canceled) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
@@ -91,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
|
||||
@@ -108,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,8 +16,8 @@ export class DynamicProjectConfigContextField extends ContextField<DynamicConfig
|
||||
nonInteractive,
|
||||
sessionManager,
|
||||
}: ContextOptions): Promise<DynamicConfigContextFn> {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
return async (options?: ExpoConfigOptions) => {
|
||||
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
|
||||
const expBefore = getExpoConfig(projectDir, options);
|
||||
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
|
||||
nonInteractive,
|
||||
|
||||
+22
@@ -252,4 +252,26 @@ describe(getProjectIdAsync, () => {
|
||||
getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('throws if extra.eas.projectId is not a string', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@notnotbrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'notnotbrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: "extra.eas.projectId" must be a string, found number. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
|
||||
);
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { getVcsClient, setVcsClient } from '../../../vcs';
|
||||
import GitClient from '../../../vcs/clients/git';
|
||||
|
||||
async function applyCliConfigAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
|
||||
if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
|
||||
throw new Error(
|
||||
|
||||
@@ -89,6 +89,12 @@ export async function getProjectIdAsync(
|
||||
|
||||
const localProjectId = exp.extra?.eas?.projectId;
|
||||
if (localProjectId) {
|
||||
if (typeof localProjectId !== 'string') {
|
||||
throw new Error(
|
||||
`Project config: "extra.eas.projectId" must be a string, found ${typeof localProjectId}. If you're not sure how to set it up on your own, remove the property entirely and it will be automatically configured on the next EAS CLI run.`
|
||||
);
|
||||
}
|
||||
|
||||
// check that the local project ID matches account and slug
|
||||
const appForProjectId = await AppQuery.byIdAsync(graphqlClient, localProjectId);
|
||||
if (exp.owner && exp.owner !== appForProjectId.ownerAccount.name) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -251,7 +251,7 @@ export async function handleDeprecatedEasJsonAsync(
|
||||
if (!(await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir)))) {
|
||||
return;
|
||||
}
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
const platformAndProfileNames: [Platform, string][] = profileNames.flatMap(profileName => [
|
||||
[Platform.ANDROID, profileName],
|
||||
|
||||
@@ -6,6 +6,8 @@ import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { setVcsClient } from '../../vcs';
|
||||
import GitNoCommitClient from '../../vcs/clients/gitNoCommit';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS Build workers, when building
|
||||
@@ -51,6 +53,8 @@ export default class BuildInternal extends EasCommand {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
setVcsClient(new GitNoCommitClient());
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
|
||||
@@ -121,7 +121,7 @@ export default class BuildResign extends EasCommand {
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasBuild]);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
});
|
||||
|
||||
const platform = await selectPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
|
||||
@@ -71,7 +71,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
});
|
||||
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.platform);
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await ensureVersionSourceIsRemoteAsync(easJsonAccessor);
|
||||
|
||||
const platforms = toPlatforms(requestedPlatform);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import { BranchMapping, getBranchMapping } from '../../channel/utils';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -132,13 +134,11 @@ async function startRolloutAsync(
|
||||
);
|
||||
}
|
||||
|
||||
const logMessage = `️Started a rollout of branch ${chalk.bold(
|
||||
const logMessage = `Started a rollout of branch ${chalk.bold(branchName)} on channel ${chalk.bold(
|
||||
channelName!
|
||||
)}! ${chalk.bold(percent)}% of users will be directed to branch ${chalk.bold(
|
||||
branchName
|
||||
)} onto channel ${chalk.bold(channelName!)}! ${chalk.bold(
|
||||
percent
|
||||
)}% of users will be directed to branch ${chalk.bold(branchName)}, ${chalk.bold(
|
||||
100 - percent
|
||||
)}% to branch ${chalk.bold(oldBranch.name)}.`;
|
||||
)}, ${chalk.bold(100 - percent)}% to branch ${chalk.bold(oldBranch.name)}.`;
|
||||
|
||||
return { newChannelInfo, logMessage };
|
||||
}
|
||||
@@ -190,7 +190,7 @@ async function editRolloutAsync(
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
|
||||
const logMessage = `️Rollout of branch ${chalk.bold(newBranch.name)} onto channel ${chalk.bold(
|
||||
const logMessage = `Rollout of branch ${chalk.bold(newBranch.name)} on channel ${chalk.bold(
|
||||
channelName!
|
||||
)} updated from ${chalk.bold(currentPercent)}% to ${chalk.bold(percent)}%. ${chalk.bold(
|
||||
percent
|
||||
@@ -256,11 +256,11 @@ async function endRolloutAsync(
|
||||
'Ending the rollout will send all traffic to a single branch. Which one should that be?',
|
||||
[
|
||||
{
|
||||
title: `${newBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
|
||||
title: `${newBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
|
||||
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
@@ -284,7 +284,7 @@ async function endRolloutAsync(
|
||||
channelId: channel.id,
|
||||
branchMapping: JSON.stringify(newBranchMapping),
|
||||
});
|
||||
const logMessage = `️Rollout on channel ${chalk.bold(
|
||||
const logMessage = `Rollout on channel ${chalk.bold(
|
||||
channelName
|
||||
)} ended. All traffic is now sent to branch ${chalk.bold(
|
||||
endOnNewBranch ? newBranch.name : oldBranch.name
|
||||
@@ -294,14 +294,12 @@ async function endRolloutAsync(
|
||||
}
|
||||
|
||||
export default class ChannelRollout extends EasCommand {
|
||||
static override hidden = true;
|
||||
static override description = 'Rollout a new branch out to a channel incrementally.';
|
||||
static override description = 'Roll a new branch out on a channel incrementally.';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'channel',
|
||||
required: true,
|
||||
description: 'rollout that the channel is on',
|
||||
description: 'channel on which the rollout should be done',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -311,7 +309,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
required: false,
|
||||
}),
|
||||
percent: Flags.integer({
|
||||
description: 'percent of traffic to redirect to the new branch',
|
||||
description: 'percent of users to send to the new branch',
|
||||
required: false,
|
||||
}),
|
||||
end: Flags.boolean({
|
||||
@@ -328,7 +326,7 @@ export default class ChannelRollout extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
args: { channel: channelName },
|
||||
args: { channel: channelNameArg },
|
||||
flags: {
|
||||
json: jsonFlag,
|
||||
end: endFlag,
|
||||
@@ -349,9 +347,23 @@ export default class ChannelRollout extends EasCommand {
|
||||
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
let channelName: string = channelNameArg;
|
||||
if (!channelName) {
|
||||
const { name } = await selectChannelOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
selectionPromptTitle: 'Select a channel on which to perform a rollout',
|
||||
paginatedQueryOptions: {
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
channelName = name;
|
||||
}
|
||||
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
channelName: channelName!,
|
||||
channelName,
|
||||
});
|
||||
if (!channel) {
|
||||
throw new Error(
|
||||
@@ -396,7 +408,15 @@ export default class ChannelRollout extends EasCommand {
|
||||
if (!isRollout) {
|
||||
rolloutMutationResult = await startRolloutAsync(graphqlClient, {
|
||||
channelName,
|
||||
branchName: branchName ?? (await promptForBranchNameAsync(channelName, nonInteractive)),
|
||||
branchName:
|
||||
branchName ??
|
||||
(await promptForBranchNameAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelName,
|
||||
nonInteractive,
|
||||
json: jsonFlag,
|
||||
})),
|
||||
percent,
|
||||
nonInteractive,
|
||||
projectId,
|
||||
@@ -433,18 +453,36 @@ export default class ChannelRollout extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
async function promptForBranchNameAsync(
|
||||
channelName: string,
|
||||
nonInteractive: boolean
|
||||
): Promise<string> {
|
||||
async function promptForBranchNameAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelName,
|
||||
nonInteractive,
|
||||
json,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
channelName: string;
|
||||
nonInteractive: boolean;
|
||||
json: boolean;
|
||||
}): Promise<string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply branch flag in non-interactive mode');
|
||||
}
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: `Select a branch to rollout onto ${channelName}`,
|
||||
validate: value => (value ? true : 'A branch must be specified.'),
|
||||
|
||||
const { name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like roll out on ${channelName}?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: updateBranch.name,
|
||||
}),
|
||||
// discard limit and offset because this query is not their intended target
|
||||
paginatedQueryOptions: {
|
||||
json,
|
||||
nonInteractive,
|
||||
offset: 0,
|
||||
},
|
||||
});
|
||||
return name;
|
||||
|
||||
return branchName;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default class Config extends EasCommand {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor(projectDir);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const profileName =
|
||||
maybeProfile ??
|
||||
(await selectAsync(
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MetadataLint extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class MetadataPull extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ export default class MetadataPush extends EasCommand {
|
||||
|
||||
const submitProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms: [Platform.IOS],
|
||||
profileName: flags.profile,
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ export default class Submit extends EasCommand {
|
||||
const platforms = toPlatforms(flagsWithPlatform.requestedPlatform);
|
||||
const submissionProfiles = await getProfilesAsync({
|
||||
type: 'submit',
|
||||
easJsonAccessor: new EasJsonAccessor(projectDir),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
|
||||
platforms,
|
||||
profileName: flagsWithPlatform.profile,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
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 nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
@@ -29,6 +37,7 @@ const updateStub: UpdateFragment = {
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
@@ -58,7 +67,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 () => {
|
||||
@@ -80,7 +92,7 @@ describe(UpdatePublish.name, () => {
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdatePublish(flags, commandOptions).run()).rejects.toThrow(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -136,14 +148,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 = nullthrows(publishData.updateInfoGroup).ios!.extra.expoClient;
|
||||
|
||||
// Ensure the basic properties are present
|
||||
expect(expoConfig).toHaveProperty('name', appJson.expo.name);
|
||||
expect(expoConfig).toHaveProperty('slug', appJson.expo.slug);
|
||||
// Ensure non-public config is not present
|
||||
expect(expoConfig).not.toHaveProperty('hooks');
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
expoConfig = {},
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
} = {}): { projectId: string } {
|
||||
expoConfig?: Partial<ExpoConfig>;
|
||||
} = {}): { projectId: string; appJson: AppJSONConfig } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
@@ -163,6 +220,7 @@ function mockTestProject({
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
...expoConfig,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,11 +238,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 +273,7 @@ function mockTestProject({
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId };
|
||||
return { projectId: configuredProjectId, appJson: appJSON };
|
||||
}
|
||||
|
||||
/** Create a new in-memory export of the project */
|
||||
@@ -225,8 +299,8 @@ function mockTestExport({
|
||||
);
|
||||
}
|
||||
|
||||
jest.mocked(collectAssetsAsync).mockResolvedValue({
|
||||
collectedAssets: new Map(
|
||||
jest.mocked(collectAssetsAsync).mockResolvedValue(
|
||||
Object.fromEntries(
|
||||
platforms.map(platform => [
|
||||
platform,
|
||||
{
|
||||
@@ -237,9 +311,8 @@ function mockTestExport({
|
||||
},
|
||||
},
|
||||
])
|
||||
),
|
||||
excludedAssets: [],
|
||||
});
|
||||
)
|
||||
);
|
||||
|
||||
jest.mocked(uploadAssetsAsync).mockResolvedValue({
|
||||
// platforms are mocked all containing only the launch asset
|
||||
|
||||
@@ -27,6 +27,7 @@ const updateStub: UpdateFragment = {
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
import {
|
||||
AppJSONConfig,
|
||||
ExpoConfig,
|
||||
GetConfigOptions,
|
||||
PackageJSONConfig,
|
||||
getConfig,
|
||||
} from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { ensureBranchExistsAsync } from '../../../branch/queries';
|
||||
import { DynamicProjectConfigContextField } from '../../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
|
||||
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { UpdateFragment } from '../../../graphql/generated';
|
||||
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
|
||||
import UpdateRollBackToEmbedded from '../roll-back-to-embedded';
|
||||
|
||||
const projectRoot = '/test-project';
|
||||
const commandOptions = { root: projectRoot } as any;
|
||||
const updateStub: UpdateFragment = {
|
||||
id: 'update-1234',
|
||||
group: 'group-1234',
|
||||
branch: { id: 'branch-1234', name: 'main' },
|
||||
message: 'test message',
|
||||
runtimeVersion: 'exposdk:47.0.0',
|
||||
platform: 'ios',
|
||||
gitCommitHash: 'commit',
|
||||
manifestFragment: JSON.stringify({ fake: 'manifest' }),
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
jest.mock('@expo/config-plugins');
|
||||
jest.mock('../../../branch/queries');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
|
||||
jest.mock('../../../update/configure');
|
||||
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
|
||||
jest.mock('../../../graphql/mutations/PublishMutation');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/publish', () => ({
|
||||
...jest.requireActual('../../../project/publish'),
|
||||
buildBundlesAsync: jest.fn(),
|
||||
collectAssetsAsync: jest.fn(),
|
||||
resolveInputDirectoryAsync: jest.fn((inputDir = 'dist') => path.join(projectRoot, inputDir)),
|
||||
uploadAssetsAsync: jest.fn(),
|
||||
}));
|
||||
|
||||
describe(UpdateRollBackToEmbedded.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
|
||||
it('errors with both --channel and --branch', async () => {
|
||||
const flags = ['--channel=channel123', '--branch=branch123'];
|
||||
|
||||
mockTestProject();
|
||||
|
||||
await expect(new UpdateRollBackToEmbedded(flags, commandOptions).run()).rejects.toThrow(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --branch, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with --non-interactive, --channel, and --message', async () => {
|
||||
const flags = ['--non-interactive', '--channel=channel123', '--message=abc'];
|
||||
|
||||
const { projectId } = mockTestProject();
|
||||
const platforms = ['android', 'ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
jest.mocked(getBranchNameFromChannelNameAsync).mockResolvedValue('branchFromChannel');
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
...updateStub,
|
||||
runtimeVersion,
|
||||
platform,
|
||||
}))
|
||||
);
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
expect(ensureBranchExistsAsync).toHaveBeenCalledWith(
|
||||
expect.any(Object), // graphql client
|
||||
{
|
||||
appId: projectId,
|
||||
branchName: 'branchFromChannel',
|
||||
}
|
||||
);
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a roll back to embedded with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
// Add configuration to the project that should not be included in the update
|
||||
mockTestProject({
|
||||
expoConfig: {
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
file: 'custom-hook.js',
|
||||
config: { some: 'config' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const platforms = ['ios'];
|
||||
const runtimeVersion = 'exposdk:47.0.0';
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdateRollBackToEmbedded(flags, commandOptions).run();
|
||||
|
||||
// Pull the publish data from the mocked publish function
|
||||
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
|
||||
expect(nullthrows(publishData.rollBackToEmbeddedInfoGroup).ios).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
expoConfig = {},
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
expoConfig?: Partial<ExpoConfig>;
|
||||
} = {}): { projectId: string; appJson: AppJSONConfig } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
description: 'fake description',
|
||||
main: 'index.js',
|
||||
};
|
||||
|
||||
const appJSON: AppJSONConfig = {
|
||||
expo: {
|
||||
name: 'testing 123',
|
||||
version: '0.1.0',
|
||||
slug: 'testing-123',
|
||||
sdkVersion: '33.0.0',
|
||||
owner: jester.accounts[0].name,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
...expoConfig,
|
||||
},
|
||||
};
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'package.json': JSON.stringify(packageJSON),
|
||||
'app.json': JSON.stringify(appJSON),
|
||||
},
|
||||
projectRoot
|
||||
);
|
||||
|
||||
const mockManifest = { exp: appJSON.expo };
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest
|
||||
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue(async (options?: GetConfigOptions) => {
|
||||
let exp = { ...mockManifest.exp };
|
||||
|
||||
// Fake a limited public config
|
||||
if (options?.isPublicConfig) {
|
||||
exp = {
|
||||
name: mockManifest.exp.name,
|
||||
version: mockManifest.exp.version,
|
||||
slug: mockManifest.exp.slug,
|
||||
sdkVersion: mockManifest.exp.sdkVersion,
|
||||
owner: mockManifest.exp.owner,
|
||||
extra: mockManifest.exp.extra,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
};
|
||||
});
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
featureGating: new FeatureGating({}, new FeatureGateEnvOverrides()),
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId, appJson: appJSON };
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ExpoConfig, Platform as PublishPlatform } from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Platform as PublishPlatform } from '@expo/config';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ensureBranchExistsAsync, selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../../branch/utils';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -23,32 +19,32 @@ import {
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../../platform';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
ExpoCLIExportPlatformFlag,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
defaultPublishPlatforms,
|
||||
filterExportedPlatformsByFlag,
|
||||
getBranchNameForCommandAsync,
|
||||
getRequestedPlatform,
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions,
|
||||
getRuntimeVersionObjectAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
isUploadedAssetCountAboveWarningThreshold,
|
||||
resolveInputDirectoryAsync,
|
||||
uploadAssetsAsync,
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import {
|
||||
formatUpdateMessage,
|
||||
getUpdateGroupJsonInfo,
|
||||
truncateString as truncateUpdateMessage,
|
||||
} from '../../update/utils';
|
||||
import { getUpdateGroupJsonInfo } from '../../update/utils';
|
||||
import { maybeWarnAboutEasPlanOverageAsync } from '../../utils/billing';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getManifestBodyAsync,
|
||||
signManifestBody,
|
||||
signBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
@@ -56,8 +52,6 @@ import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
export const defaultPublishPlatforms: Partial<PublishPlatform>[] = ['android', 'ios'];
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
@@ -88,21 +82,6 @@ type UpdateFlags = {
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
function getRequestedPlatform(platform: ExpoCLIExportPlatformFlag): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
static override description = 'publish an update group';
|
||||
|
||||
@@ -166,18 +145,18 @@ export default class UpdatePublish extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
let {
|
||||
const {
|
||||
auto: autoFlag,
|
||||
platform: platformFlag,
|
||||
channelName,
|
||||
updateMessage,
|
||||
channelName: channelNameArg,
|
||||
updateMessage: updateMessageArg,
|
||||
inputDir,
|
||||
skipBundler,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
branchName: branchNameArg,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
let branchName = this.sanitizeFlags(rawFlags).branchName;
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
@@ -198,10 +177,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasPlanOverageAsync(graphqlClient, projectId);
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
@@ -211,75 +187,28 @@ export default class UpdatePublish extends EasCommand {
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync({});
|
||||
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
const branchName = await getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
if (channelName && branchName) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto'
|
||||
);
|
||||
}
|
||||
|
||||
if (channelName) {
|
||||
branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
|
||||
}
|
||||
|
||||
if (!branchName) {
|
||||
if (autoFlag) {
|
||||
branchName = await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode');
|
||||
} else {
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to publish on?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: `${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
}),
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
} catch {
|
||||
// unable to select a branch (network error or no branches for project)
|
||||
({ name: branchName } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
validate: value => (value ? true : 'Branch name may not be empty.'),
|
||||
}));
|
||||
}
|
||||
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateMessage && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
({ updateMessage } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessage',
|
||||
message: `Provide an update message.`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
}));
|
||||
}
|
||||
const updateMessage = await getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
});
|
||||
|
||||
// build bundle and upload assets for a new publish
|
||||
if (!skipBundler) {
|
||||
@@ -300,11 +229,12 @@ export default class UpdatePublish extends EasCommand {
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
try {
|
||||
const { collectedAssets, excludedAssets } = await collectAssetsAsync(projectDir, distRoot);
|
||||
const collectedAssets = await collectAssetsAsync(distRoot);
|
||||
const assets = filterExportedPlatformsByFlag(collectedAssets, platformFlag);
|
||||
realizedPlatforms = Array.from(assets.keys());
|
||||
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
|
||||
|
||||
const uploadResults = await uploadAssetsAsync(
|
||||
graphqlClient,
|
||||
@@ -330,13 +260,6 @@ export default class UpdatePublish extends EasCommand {
|
||||
Log.withTick(
|
||||
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
|
||||
);
|
||||
if (excludedAssets.length > 0) {
|
||||
Log.withTick(
|
||||
`Excluded ${excludedAssets.length} ${
|
||||
excludedAssets.length === 1 ? 'asset' : 'assets'
|
||||
} from upload`
|
||||
);
|
||||
}
|
||||
if (uploadedNormalAssetCount === 0) {
|
||||
Log.withTick(`Uploading assets skipped - no new assets found`);
|
||||
} else {
|
||||
@@ -358,22 +281,9 @@ export default class UpdatePublish extends EasCommand {
|
||||
throw e;
|
||||
}
|
||||
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage!, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
const runtimeToPlatformMapping =
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
@@ -408,14 +318,13 @@ export default class UpdatePublish extends EasCommand {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion,
|
||||
message: truncatedMessage,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
@@ -426,7 +335,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
|
||||
const updatesTemp = [...newUpdates];
|
||||
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
|
||||
const newUpdates = updatesTemp.splice(0, Object.keys(updateGroup.updateInfoGroup).length);
|
||||
const newUpdates = updatesTemp.splice(
|
||||
0,
|
||||
Object.keys(nullthrows(updateGroup.updateInfoGroup)).length
|
||||
);
|
||||
return {
|
||||
updateGroup,
|
||||
newUpdates,
|
||||
@@ -446,11 +358,13 @@ export default class UpdatePublish extends EasCommand {
|
||||
checkManifestBodyAgainstUpdateInfoGroup(
|
||||
manifestBody,
|
||||
nullthrows(
|
||||
updateGroup.updateInfoGroup[newUpdate.platform as keyof UpdateInfoGroup]
|
||||
nullthrows(updateGroup.updateInfoGroup)[
|
||||
newUpdate.platform as keyof UpdateInfoGroup
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
const manifestSignature = signManifestBody(manifestBody, codeSigningInfo);
|
||||
const manifestSignature = signBody(manifestBody, codeSigningInfo);
|
||||
|
||||
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
|
||||
alg: codeSigningInfo.codeSigningMetadata.alg,
|
||||
@@ -512,7 +426,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: truncatedMessage },
|
||||
{ label: 'Message', value: updateMessage },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
@@ -583,41 +497,3 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platforms: PublishPlatform[],
|
||||
projectDir: string
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as Platform)) === Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ type UpdateToRepublish = {
|
||||
>;
|
||||
|
||||
export default class UpdateRepublish extends EasCommand {
|
||||
static override description = 'rollback to an existing update';
|
||||
static override description = 'roll back to an existing update';
|
||||
|
||||
static override flags = {
|
||||
channel: Flags.string({
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import { Platform as PublishPlatform } from '@expo/config';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import { ensureChannelExistsAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import fetch from '../../fetch';
|
||||
import {
|
||||
PublishUpdateGroupInput,
|
||||
StatuspageServiceName,
|
||||
UpdatePublishMutation,
|
||||
} from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
ExpoCLIExportPlatformFlag,
|
||||
defaultPublishPlatforms,
|
||||
getBranchNameForCommandAsync,
|
||||
getRequestedPlatform,
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions,
|
||||
getRuntimeVersionObjectAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
} from '../../project/publish';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getUpdateGroupJsonInfo } from '../../update/utils';
|
||||
import {
|
||||
CodeSigningInfo,
|
||||
checkDirectiveBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
getDirectiveBodyAsync,
|
||||
signBody,
|
||||
} from '../../utils/code-signing';
|
||||
import uniqBy from '../../utils/expodash/uniqBy';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
import { getVcsClient } from '../../vcs';
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
channel?: string;
|
||||
message?: string;
|
||||
platform: string;
|
||||
'private-key-path'?: string;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
auto: boolean;
|
||||
platform: ExpoCLIExportPlatformFlag;
|
||||
branchName?: string;
|
||||
channelName?: string;
|
||||
updateMessage?: string;
|
||||
privateKeyPath?: string;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
};
|
||||
|
||||
export default class UpdateRollBackToEmbedded extends EasCommand {
|
||||
static override hidden = true; // until we launch
|
||||
static override description = 'roll back to the embedded update';
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({
|
||||
description: 'Branch to publish the rollback to embedded update group on',
|
||||
required: false,
|
||||
}),
|
||||
channel: Flags.string({
|
||||
description: 'Channel that the published rollback to embedded update should affect',
|
||||
required: false,
|
||||
}),
|
||||
message: Flags.string({
|
||||
description: 'A short message describing the rollback to embedded update',
|
||||
required: false,
|
||||
}),
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: [...defaultPublishPlatforms, 'all'],
|
||||
default: 'all',
|
||||
required: false,
|
||||
}),
|
||||
auto: Flags.boolean({
|
||||
description:
|
||||
'Use the current git branch and commit message for the EAS branch and update message',
|
||||
default: false,
|
||||
}),
|
||||
'private-key-path': Flags.string({
|
||||
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
|
||||
required: false,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdateRollBackToEmbedded);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
const {
|
||||
auto: autoFlag,
|
||||
platform: platformFlag,
|
||||
channelName: channelNameArg,
|
||||
updateMessage: updateMessageArg,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
branchName: branchNameArg,
|
||||
} = this.sanitizeFlags(rawFlags);
|
||||
|
||||
const {
|
||||
getDynamicProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(UpdateRollBackToEmbedded, {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
projectId,
|
||||
projectDir,
|
||||
} = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
await maybeWarnAboutEasOutagesAsync(graphqlClient, [StatuspageServiceName.EasUpdate]);
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
|
||||
exp: expPossiblyWithoutEasUpdateConfigured,
|
||||
platform: getRequestedPlatform(platformFlag),
|
||||
projectDir,
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
|
||||
const { exp: expPrivate } = await getDynamicProjectConfigAsync({
|
||||
isPublicConfig: false,
|
||||
});
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
const branchName = await getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
|
||||
const updateMessage = await getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
});
|
||||
|
||||
const realizedPlatforms: PublishPlatform[] =
|
||||
platformFlag === 'all' ? defaultPublishPlatforms : [platformFlag];
|
||||
|
||||
const { branchId, createdBranch } = await ensureBranchExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchName,
|
||||
});
|
||||
if (createdBranch) {
|
||||
await ensureChannelExistsAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
branchId,
|
||||
channelName: branchName,
|
||||
});
|
||||
}
|
||||
|
||||
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
|
||||
|
||||
const vcsClient = getVcsClient();
|
||||
|
||||
const gitCommitHash = await vcsClient.getCommitHashAsync();
|
||||
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
|
||||
|
||||
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
|
||||
|
||||
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
|
||||
const publishSpinner = ora('Publishing...').start();
|
||||
try {
|
||||
newUpdates = await this.publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
codeSigningInfo,
|
||||
runtimeVersions,
|
||||
realizedPlatforms,
|
||||
});
|
||||
publishSpinner.succeed('Published!');
|
||||
} catch (e) {
|
||||
publishSpinner.fail('Failed to publish updates');
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(getUpdateGroupJsonInfo(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
'👉 Since multiple runtime versions are defined, multiple update groups have been published.'
|
||||
);
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
for (const runtime of uniqBy(runtimeVersions, version => version.runtimeVersion)) {
|
||||
const newUpdatesForRuntimeVersion = newUpdates.filter(
|
||||
update => update.runtimeVersion === runtime.runtimeVersion
|
||||
);
|
||||
if (newUpdatesForRuntimeVersion.length === 0) {
|
||||
throw new Error(
|
||||
`Publish response is missing updates with runtime ${runtime.runtimeVersion}.`
|
||||
);
|
||||
}
|
||||
const platforms = newUpdatesForRuntimeVersion.map(update => update.platform);
|
||||
const newAndroidUpdate = newUpdatesForRuntimeVersion.find(
|
||||
update => update.platform === 'android'
|
||||
);
|
||||
const newIosUpdate = newUpdatesForRuntimeVersion.find(update => update.platform === 'ios');
|
||||
const updateGroupId = newUpdatesForRuntimeVersion[0].group;
|
||||
|
||||
const projectName = exp.slug;
|
||||
const accountName = (await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name;
|
||||
const updateGroupUrl = getUpdateGroupUrl(accountName, projectName, updateGroupId);
|
||||
const updateGroupLink = link(updateGroupUrl, { dim: false });
|
||||
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Branch', value: branchName },
|
||||
{ label: 'Runtime version', value: runtime.runtimeVersion },
|
||||
{ label: 'Platform', value: platforms.join(', ') },
|
||||
{ label: 'Update group ID', value: updateGroupId },
|
||||
...(newAndroidUpdate
|
||||
? [{ label: 'Android update ID', value: newAndroidUpdate.id }]
|
||||
: []),
|
||||
...(newIosUpdate ? [{ label: 'iOS update ID', value: newIosUpdate.id }] : []),
|
||||
{ label: 'Message', value: updateMessage },
|
||||
...(gitCommitHash
|
||||
? [
|
||||
{
|
||||
label: 'Commit',
|
||||
value: `${gitCommitHash}${isGitWorkingTreeDirty ? '*' : ''}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{ label: 'Website link', value: updateGroupLink },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async publishRollbacksAsync({
|
||||
graphqlClient,
|
||||
isGitWorkingTreeDirty,
|
||||
gitCommitHash,
|
||||
updateMessage,
|
||||
branchId,
|
||||
codeSigningInfo,
|
||||
runtimeVersions,
|
||||
realizedPlatforms,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
isGitWorkingTreeDirty: boolean | undefined;
|
||||
gitCommitHash: string | undefined;
|
||||
updateMessage: string;
|
||||
branchId: string;
|
||||
codeSigningInfo: CodeSigningInfo | undefined;
|
||||
runtimeVersions: { platform: string; runtimeVersion: string }[];
|
||||
realizedPlatforms: PublishPlatform[];
|
||||
}): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
|
||||
const runtimeToPlatformMapping =
|
||||
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
|
||||
const rollbackInfoGroups = Object.fromEntries(
|
||||
realizedPlatforms.map(platform => [platform, true])
|
||||
);
|
||||
|
||||
// Sort the updates into different groups based on their platform specific runtime versions
|
||||
const updateGroups: PublishUpdateGroupInput[] = runtimeToPlatformMapping.map(
|
||||
({ runtimeVersion, platforms }) => {
|
||||
const localRollbackInfoGroup = Object.fromEntries(
|
||||
platforms.map(platform => [platform, rollbackInfoGroups[platform]])
|
||||
);
|
||||
|
||||
return {
|
||||
branchId,
|
||||
rollBackToEmbeddedInfoGroup: localRollbackInfoGroup,
|
||||
runtimeVersion,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const newUpdates = await PublishMutation.publishUpdateGroupAsync(graphqlClient, updateGroups);
|
||||
|
||||
if (codeSigningInfo) {
|
||||
Log.log('🔒 Signing roll back');
|
||||
|
||||
const updatesTemp = [...newUpdates];
|
||||
const updateGroupsAndTheirUpdates = updateGroups.map(updateGroup => {
|
||||
const newUpdates = updatesTemp.splice(
|
||||
0,
|
||||
Object.keys(nullthrows(updateGroup.rollBackToEmbeddedInfoGroup)).length
|
||||
);
|
||||
return {
|
||||
updateGroup,
|
||||
newUpdates,
|
||||
};
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
updateGroupsAndTheirUpdates.map(async ({ newUpdates }) => {
|
||||
await Promise.all(
|
||||
newUpdates.map(async newUpdate => {
|
||||
const response = await fetch(newUpdate.manifestPermalink, {
|
||||
method: 'GET',
|
||||
headers: { accept: 'multipart/mixed' },
|
||||
});
|
||||
const directiveBody = nullthrows(await getDirectiveBodyAsync(response));
|
||||
|
||||
checkDirectiveBodyAgainstUpdateInfoGroup(directiveBody);
|
||||
|
||||
const directiveSignature = signBody(directiveBody, codeSigningInfo);
|
||||
|
||||
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, newUpdate.id, {
|
||||
alg: codeSigningInfo.codeSigningMetadata.alg,
|
||||
keyid: codeSigningInfo.codeSigningMetadata.keyid,
|
||||
sig: directiveSignature,
|
||||
});
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return newUpdates;
|
||||
}
|
||||
|
||||
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
|
||||
const nonInteractive = flags['non-interactive'] ?? false;
|
||||
|
||||
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
|
||||
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
|
||||
Errors.error(
|
||||
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
|
||||
{ exit: 1 }
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
auto,
|
||||
branchName,
|
||||
channelName,
|
||||
updateMessage,
|
||||
platform: flags.platform as RequestedPlatform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
nonInteractive,
|
||||
json: flags.json ?? false,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export default class IosCredentialsProvider {
|
||||
private async disablePushNotificationsSetupInEasJsonAsync(
|
||||
ctx: CredentialsContext
|
||||
): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(ctx.projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(ctx.projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
easJsonRawObject.cli = {
|
||||
|
||||
@@ -8,7 +8,7 @@ export class SelectBuildProfileFromEasJson<T extends Platform> {
|
||||
private easJsonAccessor: EasJsonAccessor;
|
||||
|
||||
constructor(projectDir: string, private platform: T) {
|
||||
this.easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
this.easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
}
|
||||
|
||||
async runAsync(): Promise<BuildProfile<T>> {
|
||||
|
||||
+427
-107
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,66 @@
|
||||
import assert from 'assert';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import { BillingUpdateByAccountQuery } from '../generated';
|
||||
|
||||
export const BillingUpdateQuery = {
|
||||
async byAccountIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
date: Date
|
||||
): Promise<BillingUpdateByAccountQuery['account']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<BillingUpdateByAccountQuery>(
|
||||
gql`
|
||||
query BillingUpdateByAccountQuery($accountId: String!, $date: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
billing {
|
||||
id
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
meteredBillingStatus {
|
||||
EAS_UPDATE
|
||||
}
|
||||
}
|
||||
}
|
||||
usageMetrics {
|
||||
byBillingPeriod(date: $date, service: UPDATES) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
metricType
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId, date },
|
||||
{
|
||||
additionalTypenames: [
|
||||
'Account',
|
||||
'Billing',
|
||||
'SubscriptionDetails',
|
||||
'AccountUsageMetrics',
|
||||
'UsageMetricTotal',
|
||||
'EstimatedUsage',
|
||||
],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
assert(data.account, 'GraphQL: `account` not defined in server response');
|
||||
return data.account.byId;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const AccountUpdatesUsageFragmentNode = gql`
|
||||
fragment AccountUpdatesUsageFragment on Account {
|
||||
id
|
||||
billing {
|
||||
id
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
meteredBillingStatus {
|
||||
EAS_UPDATE
|
||||
}
|
||||
}
|
||||
}
|
||||
usageMetrics {
|
||||
byBillingPeriod(date: $date, service: UPDATES) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
metricType
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
isRollBackToEmbedded
|
||||
manifestPermalink
|
||||
gitCommitHash
|
||||
actor {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EasJsonUtils } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import mockdate from 'mockdate';
|
||||
import path from 'path';
|
||||
@@ -6,16 +5,15 @@ import { instance, mock } from 'ts-mockito';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { defaultPublishPlatforms } from '../../commands/update';
|
||||
import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import {
|
||||
CollectedAssets,
|
||||
MetadataJoi,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
defaultPublishPlatforms,
|
||||
filterExportedPlatformsByFlag,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getAssetHashFromPath,
|
||||
@@ -30,17 +28,6 @@ import {
|
||||
|
||||
jest.mock('../../uploads');
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/eas-json', () => {
|
||||
const actual = jest.requireActual('@expo/eas-json');
|
||||
|
||||
const EasJsonUtilsMock = {
|
||||
getUpdatesConfigAsync: jest.fn(),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
EasJsonUtils: EasJsonUtilsMock,
|
||||
};
|
||||
});
|
||||
|
||||
const dummyFileBuffer = Buffer.from('dummy-file');
|
||||
|
||||
@@ -109,38 +96,20 @@ describe('MetadataJoi', () => {
|
||||
|
||||
describe(filterExportedPlatformsByFlag, () => {
|
||||
it(`returns all`, () => {
|
||||
expect(
|
||||
filterExportedPlatformsByFlag(
|
||||
new Map([
|
||||
['web', {}],
|
||||
['ios', {}],
|
||||
['android', {}],
|
||||
]),
|
||||
'all'
|
||||
)
|
||||
).toEqual(
|
||||
new Map([
|
||||
['web', {}],
|
||||
['ios', {}],
|
||||
['android', {}],
|
||||
])
|
||||
);
|
||||
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'all')).toEqual({
|
||||
web: true,
|
||||
ios: true,
|
||||
android: true,
|
||||
});
|
||||
});
|
||||
it(`selects a platform`, () => {
|
||||
expect(
|
||||
filterExportedPlatformsByFlag(
|
||||
new Map([
|
||||
['web', {}],
|
||||
['ios', {}],
|
||||
['android', {}],
|
||||
]),
|
||||
'ios'
|
||||
)
|
||||
).toEqual(new Map([['ios', {}]]));
|
||||
expect(filterExportedPlatformsByFlag({ web: true, ios: true, android: true }, 'ios')).toEqual({
|
||||
ios: true,
|
||||
});
|
||||
});
|
||||
it(`asserts selected platform missing`, () => {
|
||||
expect(() =>
|
||||
filterExportedPlatformsByFlag(new Map([['web', {}]]), 'ios')
|
||||
filterExportedPlatformsByFlag({ web: true }, 'ios')
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"--platform="ios" not found in metadata.json. Available platform(s): web"`
|
||||
);
|
||||
@@ -238,25 +207,22 @@ describe(buildUnsortedUpdateInfoGroupAsync, () => {
|
||||
it('returns the correct value', async () => {
|
||||
await expect(
|
||||
buildUnsortedUpdateInfoGroupAsync(
|
||||
new Map([
|
||||
[
|
||||
'android',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
{
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
],
|
||||
{
|
||||
android: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'bundle/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
],
|
||||
]),
|
||||
assets: [
|
||||
{
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: assetPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
slug: 'hello',
|
||||
name: 'hello',
|
||||
@@ -428,155 +394,31 @@ describe(collectAssetsAsync, () => {
|
||||
})
|
||||
);
|
||||
|
||||
const projectDir = path.resolve();
|
||||
expect(await collectAssetsAsync(projectDir, inputDir)).toEqual({
|
||||
excludedAssets: [],
|
||||
collectedAssets: new Map([
|
||||
[
|
||||
'android',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/android.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
],
|
||||
[
|
||||
'ios',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/ios.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
],
|
||||
[
|
||||
'web',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/web.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it('supports asset exclusion', async () => {
|
||||
const fakeHash = 'hdahukw8adhawi8fawhfa8';
|
||||
const bundles = {
|
||||
android: 'android-bundle-code',
|
||||
ios: 'ios-bundle-code',
|
||||
web: 'web-bundle-code',
|
||||
};
|
||||
const inputDir = path.resolve(uuidv4());
|
||||
|
||||
const userDefinedAssets = [
|
||||
{
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: `${inputDir}/assets/${fakeHash}`,
|
||||
originalPath: `assets/wat.jpg`,
|
||||
expect(await collectAssetsAsync(inputDir)).toEqual({
|
||||
android: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/android.js`,
|
||||
},
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
];
|
||||
|
||||
const bundleDir = path.resolve(`${inputDir}/bundles`);
|
||||
const assetDir = path.resolve(`${inputDir}/assets`);
|
||||
await fs.mkdir(bundleDir, { recursive: true });
|
||||
await fs.mkdir(assetDir, { recursive: true });
|
||||
for (const platform of defaultPublishPlatforms) {
|
||||
await fs.writeFile(path.resolve(inputDir, `bundles/${platform}.js`), bundles[platform]);
|
||||
}
|
||||
await fs.writeFile(path.resolve(`${inputDir}/assets/${fakeHash}`), dummyFileBuffer);
|
||||
await fs.writeFile(
|
||||
path.resolve(inputDir, 'metadata.json'),
|
||||
JSON.stringify({
|
||||
version: 0,
|
||||
bundler: 'metro',
|
||||
fileMetadata: {
|
||||
android: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/android.js',
|
||||
},
|
||||
ios: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/ios.js',
|
||||
},
|
||||
web: {
|
||||
assets: [{ path: `assets/${fakeHash}`, ext: 'jpg' }],
|
||||
bundle: 'bundles/web.js',
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/ios.js`,
|
||||
},
|
||||
})
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.resolve(inputDir, 'assetmap.json'),
|
||||
JSON.stringify({
|
||||
[fakeHash]: {
|
||||
__packager_asset: true,
|
||||
fileSystemLocation: '/Users/blah/temp/assets',
|
||||
httpServerLocation: 'assets/assets',
|
||||
width: 2339,
|
||||
height: 1560,
|
||||
scales: [1],
|
||||
files: ['/Users/blah/temp/assets/wat.jpg'],
|
||||
hash: fakeHash,
|
||||
name: 'wat',
|
||||
type: 'jpg',
|
||||
fileHashes: [fakeHash],
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
web: {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/web.js`,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
jest.mocked(EasJsonUtils.getUpdatesConfigAsync).mockResolvedValue({
|
||||
assetExcludePatterns: ['**/*.jpg'],
|
||||
});
|
||||
|
||||
const projectDir = path.resolve();
|
||||
expect(await collectAssetsAsync(projectDir, inputDir)).toEqual({
|
||||
excludedAssets: userDefinedAssets,
|
||||
collectedAssets: new Map([
|
||||
[
|
||||
'android',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/android.js`,
|
||||
},
|
||||
assets: [],
|
||||
},
|
||||
],
|
||||
[
|
||||
'ios',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/ios.js`,
|
||||
},
|
||||
assets: [],
|
||||
},
|
||||
],
|
||||
[
|
||||
'web',
|
||||
{
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: `${inputDir}/bundles/web.js`,
|
||||
},
|
||||
assets: [],
|
||||
},
|
||||
],
|
||||
]),
|
||||
assets: userDefinedAssets,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -646,46 +488,40 @@ describe(uploadAssetsAsync, () => {
|
||||
path: userDefinedPath,
|
||||
};
|
||||
|
||||
const assetsForUpdateInfoGroup: CollectedAssets = new Map([
|
||||
[
|
||||
'android',
|
||||
{
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
const assetsForUpdateInfoGroup = {
|
||||
android: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
],
|
||||
[
|
||||
'ios',
|
||||
{
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
type: 'bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
],
|
||||
]);
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await fs.writeFile(androidBundlePath, publishBundles.android.code);
|
||||
|
||||
@@ -74,7 +74,7 @@ export function parseGradleCommand(cmd: string, buildGradle: AppBuildGradle): Gr
|
||||
const [moduleName, taskName] =
|
||||
splitCmd.length > 1 ? [splitCmd[0], splitCmd[1]] : [undefined, splitCmd[0]];
|
||||
|
||||
const matchResult = taskName.match(/(build|bundle|assemble)(.*)(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,47 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { errors, readAndValidateBuildConfigAsync } from '@expo/steps';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function validateCustomBuildConfigAsync(
|
||||
projectDir: string,
|
||||
profile: BuildProfile<Platform>
|
||||
): Promise<void> {
|
||||
if (!profile.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeConfigPath = getCustomBuildConfigPath(profile.config);
|
||||
const configPath = path.join(projectDir, relativeConfigPath);
|
||||
if (!(await fs.pathExists(configPath))) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(relativeConfigPath)} does not exist.`
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await readAndValidateBuildConfigAsync(configPath, { skipNamespacedFunctionsCheck: true });
|
||||
} catch (err) {
|
||||
if (err instanceof errors.BuildConfigYAMLError) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(
|
||||
relativeConfigPath
|
||||
)} contains invalid YAML.\n\n${err.message}`
|
||||
);
|
||||
} else if (err instanceof errors.BuildConfigError) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(
|
||||
relativeConfigPath
|
||||
)} contains invalid configuration. Please check the docs!\n${err.message}`
|
||||
);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomBuildConfigPath(configFilename: string): string {
|
||||
return path.join('.eas/build', configFilename);
|
||||
}
|
||||
@@ -1,28 +1,36 @@
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { Platform as EASBuildJobPlatform, Workflow } from '@expo/eas-build-job';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs-extra';
|
||||
import Joi from 'joi';
|
||||
import mime from 'mime';
|
||||
import minimatch from 'minimatch';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import { selectBranchOnAppAsync } from '../branch/queries';
|
||||
import { getDefaultBranchNameAsync } from '../branch/utils';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { AssetMetadataStatus, PartialManifestAsset } from '../graphql/generated';
|
||||
import { PublishMutation } from '../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../graphql/queries/PublishQuery';
|
||||
import Log from '../log';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { RequestedPlatform, requestedPlatformDisplayNames } from '../platform';
|
||||
import { promptAsync } from '../prompts';
|
||||
import { getBranchNameFromChannelNameAsync } from '../update/getBranchNameFromChannelNameAsync';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../update/utils';
|
||||
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
|
||||
import chunk from '../utils/expodash/chunk';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import mapMap from '../utils/expodash/mapMap';
|
||||
import mapMapAsync from '../utils/expodash/mapMapAsync';
|
||||
import partition from '../utils/expodash/partition';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { resolveWorkflowAsync } from './workflow';
|
||||
|
||||
export type ExpoCLIExportPlatformFlag = Platform | 'all';
|
||||
|
||||
@@ -41,15 +49,12 @@ export type RawAsset = {
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
export type CollectedAssets = Map<
|
||||
Platform,
|
||||
{
|
||||
type CollectedAssets = {
|
||||
[platform in Platform]?: {
|
||||
launchAsset: RawAsset;
|
||||
assets: RawAsset[];
|
||||
}
|
||||
>;
|
||||
|
||||
type ExcludedAssets = RawAsset[];
|
||||
};
|
||||
};
|
||||
|
||||
type ManifestExtra = {
|
||||
expoClient?: { [key: string]: any };
|
||||
@@ -154,21 +159,20 @@ export async function buildUnsortedUpdateInfoGroupAsync(
|
||||
assets: CollectedAssets,
|
||||
exp: ExpoConfig
|
||||
): Promise<UpdateInfoGroup> {
|
||||
return Object.fromEntries(
|
||||
(
|
||||
await mapMapAsync(assets, async (collectedAssets): Promise<ManifestFragment> => {
|
||||
return {
|
||||
launchAsset: await convertAssetToUpdateInfoGroupFormatAsync(collectedAssets.launchAsset),
|
||||
assets: await Promise.all(
|
||||
collectedAssets.assets.map(convertAssetToUpdateInfoGroupFormatAsync)
|
||||
),
|
||||
extra: {
|
||||
expoClient: exp,
|
||||
},
|
||||
};
|
||||
})
|
||||
).entries()
|
||||
) as UpdateInfoGroup;
|
||||
let platform: Platform;
|
||||
const updateInfoGroup: Partial<UpdateInfoGroup> = {};
|
||||
for (platform in assets) {
|
||||
updateInfoGroup[platform] = {
|
||||
launchAsset: await convertAssetToUpdateInfoGroupFormatAsync(assets[platform]?.launchAsset!),
|
||||
assets: await Promise.all(
|
||||
(assets[platform]?.assets ?? []).map(convertAssetToUpdateInfoGroupFormatAsync)
|
||||
),
|
||||
extra: {
|
||||
expoClient: exp,
|
||||
},
|
||||
};
|
||||
}
|
||||
return updateInfoGroup as UpdateInfoGroup;
|
||||
}
|
||||
|
||||
export async function buildBundlesAsync({
|
||||
@@ -250,25 +254,25 @@ export function loadMetadata(distRoot: string): Metadata {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export function filterExportedPlatformsByFlag<V>(
|
||||
record: Map<Platform, V>,
|
||||
export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform, any>>>(
|
||||
record: T,
|
||||
platformFlag: ExpoCLIExportPlatformFlag
|
||||
): Map<Platform, V> {
|
||||
): T {
|
||||
if (platformFlag === 'all') {
|
||||
return record;
|
||||
}
|
||||
|
||||
const platform = platformFlag as Platform;
|
||||
|
||||
if (!record.has(platform)) {
|
||||
if (!record[platform]) {
|
||||
throw new Error(
|
||||
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Array.from(
|
||||
record.keys()
|
||||
`--platform="${platform}" not found in metadata.json. Available platform(s): ${Object.keys(
|
||||
record
|
||||
).join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
return new Map([[platform, nullthrows(record.get(platform))]]);
|
||||
return { [platform]: record[platform] } as T;
|
||||
}
|
||||
|
||||
/** Try to load the asset map for logging the names of assets published */
|
||||
@@ -310,77 +314,29 @@ export function getOriginalPathFromAssetMap(
|
||||
}
|
||||
|
||||
/** Given a directory, load the metadata.json and collect the assets for each platform. */
|
||||
export async function collectAssetsAsync(
|
||||
projectDir: string,
|
||||
distRoot: string
|
||||
): Promise<{ collectedAssets: CollectedAssets; excludedAssets: ExcludedAssets }> {
|
||||
const metadata = loadMetadata(distRoot);
|
||||
const assetmap = await loadAssetMapAsync(distRoot);
|
||||
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
|
||||
const metadata = loadMetadata(dir);
|
||||
const assetmap = await loadAssetMapAsync(dir);
|
||||
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const updatesConfig = await EasJsonUtils.getUpdatesConfigAsync(easJsonAccessor);
|
||||
const assetExcludePatterns = updatesConfig?.assetExcludePatterns;
|
||||
const collectedAssets: CollectedAssets = {};
|
||||
|
||||
const platformToAssets = new Map(
|
||||
(Object.keys(metadata.fileMetadata) as Platform[]).map(platform => [
|
||||
platform,
|
||||
metadata.fileMetadata[platform].assets,
|
||||
])
|
||||
);
|
||||
|
||||
const platformToIncludedAndExcludedAssets = mapMap(platformToAssets, assets => {
|
||||
if (!assetExcludePatterns) {
|
||||
return {
|
||||
includedAssets: assets,
|
||||
excludedAssets: [],
|
||||
};
|
||||
}
|
||||
|
||||
const [includedAssets, excludedAssets] = partition(assets, asset => {
|
||||
const originalPath = getOriginalPathFromAssetMap(assetmap, asset) ?? undefined;
|
||||
return (
|
||||
!originalPath || !assetExcludePatterns.some(pattern => minimatch(originalPath, pattern))
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
includedAssets,
|
||||
excludedAssets,
|
||||
for (const platform of Object.keys(metadata.fileMetadata) as Platform[]) {
|
||||
collectedAssets[platform] = {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(dir, metadata.fileMetadata[platform].bundle),
|
||||
},
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => ({
|
||||
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
|
||||
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(dir, asset.path),
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const transformAsset = (asset: { path: string; ext: string }): RawAsset => {
|
||||
return {
|
||||
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
|
||||
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(distRoot, asset.path),
|
||||
};
|
||||
};
|
||||
|
||||
const collectedAssets = mapMap(
|
||||
platformToIncludedAndExcludedAssets,
|
||||
({ includedAssets }, platform) => {
|
||||
return {
|
||||
launchAsset: {
|
||||
fileExtension: '.bundle',
|
||||
contentType: 'application/javascript',
|
||||
path: path.resolve(distRoot, metadata.fileMetadata[platform].bundle),
|
||||
},
|
||||
assets: includedAssets.map(transformAsset),
|
||||
};
|
||||
}
|
||||
);
|
||||
const excludedAssets = uniqBy(
|
||||
Array.from(
|
||||
mapMap(platformToIncludedAndExcludedAssets, ({ excludedAssets }) =>
|
||||
excludedAssets.map(transformAsset)
|
||||
).values()
|
||||
).flat(),
|
||||
rawAsset => rawAsset.path
|
||||
);
|
||||
|
||||
return { collectedAssets, excludedAssets };
|
||||
return collectedAssets;
|
||||
}
|
||||
|
||||
// ensure the file extension has a '.' prefix
|
||||
@@ -428,11 +384,16 @@ export async function uploadAssetsAsync(
|
||||
updateSpinnerText?: (totalAssets: number, missingAssets: number) => void
|
||||
): Promise<AssetUploadResult> {
|
||||
let assets: RawAsset[] = [];
|
||||
let platform: keyof CollectedAssets;
|
||||
const launchAssets: RawAsset[] = [];
|
||||
mapMap(assetsForUpdateInfoGroup, collectedAssets => {
|
||||
launchAssets.push(collectedAssets.launchAsset);
|
||||
assets = [...assets, collectedAssets.launchAsset, ...collectedAssets.assets];
|
||||
});
|
||||
for (platform in assetsForUpdateInfoGroup) {
|
||||
launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset);
|
||||
assets = [
|
||||
...assets,
|
||||
assetsForUpdateInfoGroup[platform]!.launchAsset,
|
||||
...assetsForUpdateInfoGroup[platform]!.assets,
|
||||
];
|
||||
}
|
||||
|
||||
const assetsWithStorageKey = await Promise.all(
|
||||
assets.map(async asset => {
|
||||
@@ -506,3 +467,188 @@ export function isUploadedAssetCountAboveWarningThreshold(
|
||||
const warningThreshold = Math.floor(assetLimitPerUpdateGroup * 0.75);
|
||||
return uploadedAssetCount > warningThreshold;
|
||||
}
|
||||
|
||||
export async function getBranchNameForCommandAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
channelNameArg,
|
||||
branchNameArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
channelNameArg: string | undefined;
|
||||
branchNameArg: string | undefined;
|
||||
autoFlag: boolean;
|
||||
nonInteractive: boolean;
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}): Promise<string> {
|
||||
if (channelNameArg && branchNameArg) {
|
||||
throw new Error(
|
||||
'Cannot specify both --channel and --branch. Specify either --channel, --branch, or --auto.'
|
||||
);
|
||||
}
|
||||
|
||||
if (channelNameArg) {
|
||||
return await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelNameArg);
|
||||
}
|
||||
|
||||
if (branchNameArg) {
|
||||
return branchNameArg;
|
||||
}
|
||||
|
||||
if (autoFlag) {
|
||||
return await getDefaultBranchNameAsync();
|
||||
} else if (nonInteractive) {
|
||||
throw new Error('Must supply --channel, --branch or --auto when in non-interactive mode.');
|
||||
} else {
|
||||
let branchName: string;
|
||||
|
||||
try {
|
||||
const branch = await selectBranchOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
promptTitle: `Which branch would you like to use?`,
|
||||
displayTextForListItem: updateBranch => ({
|
||||
title: `${updateBranch.name} ${chalk.grey(
|
||||
`- current update: ${formatUpdateMessage(updateBranch.updates[0])}`
|
||||
)}`,
|
||||
}),
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
branchName = branch.name;
|
||||
} catch {
|
||||
// unable to select a branch (network error or no branches for project)
|
||||
const { name } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'No branches found. Provide a branch name:',
|
||||
initial: await getDefaultBranchNameAsync(),
|
||||
validate: value => (value ? true : 'Branch name may not be empty.'),
|
||||
});
|
||||
branchName = name;
|
||||
}
|
||||
|
||||
assert(branchName, 'Branch name must be specified.');
|
||||
return branchName;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getUpdateMessageForCommandAsync({
|
||||
updateMessageArg,
|
||||
autoFlag,
|
||||
nonInteractive,
|
||||
jsonFlag,
|
||||
}: {
|
||||
updateMessageArg: string | undefined;
|
||||
autoFlag: boolean;
|
||||
nonInteractive: boolean;
|
||||
jsonFlag: boolean;
|
||||
}): Promise<string> {
|
||||
let updateMessage = updateMessageArg;
|
||||
if (!updateMessageArg && autoFlag) {
|
||||
updateMessage = (await getVcsClient().getLastCommitMessageAsync())?.trim();
|
||||
}
|
||||
|
||||
if (!updateMessage) {
|
||||
if (nonInteractive) {
|
||||
throw new Error('Must supply --message or use --auto when in non-interactive mode');
|
||||
}
|
||||
|
||||
const validationMessage = 'publish message may not be empty.';
|
||||
if (jsonFlag) {
|
||||
throw new Error(validationMessage);
|
||||
}
|
||||
const { updateMessageLocal } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'updateMessageLocal',
|
||||
message: `Provide an update message:`,
|
||||
initial: (await getVcsClient().getLastCommitMessageAsync())?.trim(),
|
||||
validate: (value: any) => (value ? true : validationMessage),
|
||||
});
|
||||
updateMessage = updateMessageLocal;
|
||||
}
|
||||
|
||||
assert(updateMessage, 'Update message must be specified.');
|
||||
|
||||
const truncatedMessage = truncateUpdateMessage(updateMessage, 1024);
|
||||
if (truncatedMessage !== updateMessage) {
|
||||
Log.warn('Update message exceeds the allowed 1024 character limit. Truncating message...');
|
||||
}
|
||||
|
||||
return updateMessage;
|
||||
}
|
||||
|
||||
export const defaultPublishPlatforms: Platform[] = ['android', 'ios'];
|
||||
|
||||
export function getRequestedPlatform(
|
||||
platform: ExpoCLIExportPlatformFlag
|
||||
): RequestedPlatform | null {
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
return RequestedPlatform.Android;
|
||||
case 'ios':
|
||||
return RequestedPlatform.Ios;
|
||||
case 'web':
|
||||
return null;
|
||||
case 'all':
|
||||
return RequestedPlatform.All;
|
||||
default:
|
||||
throw new Error(`Unsupported platform: ${platform}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get runtime versions grouped by platform. Runtime version is always `null` on web where the platform is always backwards compatible. */
|
||||
export async function getRuntimeVersionObjectAsync(
|
||||
exp: ExpoConfig,
|
||||
platforms: Platform[],
|
||||
projectDir: string
|
||||
): Promise<{ platform: string; runtimeVersion: string }[]> {
|
||||
for (const platform of platforms) {
|
||||
if (platform === 'web') {
|
||||
continue;
|
||||
}
|
||||
const isPolicy = typeof (exp[platform]?.runtimeVersion ?? exp.runtimeVersion) === 'object';
|
||||
if (isPolicy) {
|
||||
const isManaged =
|
||||
(await resolveWorkflowAsync(projectDir, platform as EASBuildJobPlatform)) ===
|
||||
Workflow.MANAGED;
|
||||
if (!isManaged) {
|
||||
throw new Error(
|
||||
'Runtime version policies are only supported in the managed workflow. In the bare workflow, runtime version needs to be set manually.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...new Set(platforms)].map(platform => {
|
||||
if (platform === 'web') {
|
||||
return { platform: 'web', runtimeVersion: 'UNVERSIONED' };
|
||||
}
|
||||
return {
|
||||
platform,
|
||||
runtimeVersion: nullthrows(
|
||||
Updates.getRuntimeVersion(exp, platform),
|
||||
`Unable to determine runtime version for ${
|
||||
requestedPlatformDisplayNames[platform]
|
||||
}. ${learnMore('https://docs.expo.dev/eas-update/runtime-versions/')}`
|
||||
),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function getRuntimeToPlatformMappingFromRuntimeVersions(
|
||||
runtimeVersions: { platform: string; runtimeVersion: string }[]
|
||||
): { runtimeVersion: string; platforms: string[] }[] {
|
||||
const runtimeToPlatformMapping: { runtimeVersion: string; platforms: string[] }[] = [];
|
||||
for (const runtime of runtimeVersions) {
|
||||
const platforms = runtimeVersions
|
||||
.filter(({ runtimeVersion }) => runtimeVersion === runtime.runtimeVersion)
|
||||
.map(({ platform }) => platform);
|
||||
if (!runtimeToPlatformMapping.find(item => item.runtimeVersion === runtime.runtimeVersion)) {
|
||||
runtimeToPlatformMapping.push({ runtimeVersion: runtime.runtimeVersion, platforms });
|
||||
}
|
||||
}
|
||||
return runtimeToPlatformMapping;
|
||||
}
|
||||
@@ -124,6 +124,7 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
|
||||
runtime: {} as Runtime, // Temporary fix to resolve type errors
|
||||
platform: 'ios',
|
||||
manifestFragment: '...',
|
||||
isRollBackToEmbedded: false,
|
||||
activityTimestamp: '2022-12-07T02:24:43.487Z',
|
||||
branchId: '',
|
||||
awaitingCodeSigningInfo: false,
|
||||
|
||||
@@ -273,7 +273,7 @@ export async function ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir: stri
|
||||
}
|
||||
|
||||
try {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
@@ -343,7 +343,7 @@ export async function ensureEASUpdateIsConfiguredAsync(
|
||||
expWithoutUpdates.sdkVersion
|
||||
);
|
||||
if (!hasExpoUpdates) {
|
||||
await installExpoUpdatesAsync(projectDir, { silent: !Log.isDebug });
|
||||
await installExpoUpdatesAsync(projectDir, { silent: false });
|
||||
Log.withTick('Installed expo updates');
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ export type FormattedUpdateGroupDescription = {
|
||||
group: string;
|
||||
platforms: string;
|
||||
runtimeVersion: string;
|
||||
codeSigningKey: string | undefined;
|
||||
isRollBackToEmbedded: boolean;
|
||||
};
|
||||
|
||||
export type FormattedBranchDescription = {
|
||||
@@ -74,6 +76,8 @@ export function formatUpdateGroup(update: FormattedUpdateGroupDescription): stri
|
||||
{ label: 'Platforms', value: update.platforms },
|
||||
{ label: 'Runtime Version', value: update.runtimeVersion },
|
||||
{ label: 'Message', value: update.message },
|
||||
{ label: 'Code Signing Key', value: update.codeSigningKey ?? 'N/A' },
|
||||
{ label: 'Is Roll Back to Embedded', value: update.isRollBackToEmbedded ? 'Yes' : 'No' },
|
||||
{ label: 'Group ID', value: update.group },
|
||||
]);
|
||||
}
|
||||
@@ -203,6 +207,7 @@ export function getUpdateGroupJsonInfo(updateGroups: UpdateFragment[]): UpdateJs
|
||||
runtimeVersion: update.runtimeVersion,
|
||||
platform: update.platform,
|
||||
manifestPermalink: update.manifestPermalink,
|
||||
isRollBackToEmbedded: update.isRollBackToEmbedded,
|
||||
gitCommitHash: update.gitCommitHash,
|
||||
}));
|
||||
}
|
||||
@@ -213,6 +218,8 @@ export function getUpdateGroupDescriptions(
|
||||
return updateGroups.map(updateGroup => ({
|
||||
message: formatUpdateMessage(updateGroup[0]),
|
||||
runtimeVersion: updateGroup[0].runtimeVersion,
|
||||
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
|
||||
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
|
||||
group: updateGroup[0].group,
|
||||
platforms: formatPlatformForUpdateGroup(updateGroup),
|
||||
}));
|
||||
@@ -225,6 +232,8 @@ export function getUpdateGroupDescriptionsWithBranch(
|
||||
branch: updateGroup[0].branch.name,
|
||||
message: formatUpdateMessage(updateGroup[0]),
|
||||
runtimeVersion: updateGroup[0].runtimeVersion,
|
||||
isRollBackToEmbedded: updateGroup[0].isRollBackToEmbedded,
|
||||
codeSigningKey: updateGroup[0].codeSigningInfo?.keyid,
|
||||
group: updateGroup[0].group,
|
||||
platforms: formatPlatformForUpdateGroup(updateGroup),
|
||||
}));
|
||||
@@ -241,6 +250,8 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
|
||||
update: {
|
||||
message: formatUpdateMessage(latestUpdate),
|
||||
runtimeVersion: latestUpdate.runtimeVersion,
|
||||
isRollBackToEmbedded: latestUpdate.isRollBackToEmbedded,
|
||||
codeSigningKey: latestUpdate.codeSigningInfo?.keyid,
|
||||
group: latestUpdate.group,
|
||||
platforms: getPlatformsForGroup({
|
||||
group: latestUpdate.group,
|
||||
|
||||
@@ -11,6 +11,7 @@ jest.mock('@expo/eas-json', () => {
|
||||
const EasJsonUtilsMock = {
|
||||
getBuildProfileAsync: jest.fn(),
|
||||
getBuildProfileNamesAsync: jest.fn(),
|
||||
getBuildProfileDeprecationWarnings: jest.fn(() => []),
|
||||
};
|
||||
return {
|
||||
...actual,
|
||||
@@ -29,7 +30,7 @@ describe(getProfilesAsync, () => {
|
||||
});
|
||||
|
||||
it('defaults to production profile', async () => {
|
||||
const easJsonAccessor = new EasJsonAccessor('/fake');
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
|
||||
const result = await getProfilesAsync({
|
||||
easJsonAccessor,
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
@@ -51,7 +52,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
easJsonAccessor: new EasJsonAccessor('/fake'),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
|
||||
platforms: [Platform.ANDROID],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
@@ -60,7 +61,7 @@ describe(getProfilesAsync, () => {
|
||||
});
|
||||
|
||||
it('gets a specific profile', async () => {
|
||||
const easJsonAccessor = new EasJsonAccessor('/fake');
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath('/fake');
|
||||
const result = await getProfilesAsync({
|
||||
easJsonAccessor,
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
@@ -85,7 +86,7 @@ describe(getProfilesAsync, () => {
|
||||
|
||||
await expect(
|
||||
getProfilesAsync({
|
||||
easJsonAccessor: new EasJsonAccessor('/fake'),
|
||||
easJsonAccessor: EasJsonAccessor.fromProjectPath('/fake'),
|
||||
platforms: [Platform.ANDROID, Platform.IOS],
|
||||
profileName: undefined,
|
||||
type: 'build',
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { BillingUpdateQuery } from '../graphql/queries/BillingUpdateQuery';
|
||||
import Log from '../log';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
|
||||
const WARNING_THRESHOLD = 0.85;
|
||||
export async function maybeWarnAboutEasPlanOverageAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<void> {
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
const billingUpdateQuery = await BillingUpdateQuery.byAccountIdAsync(
|
||||
graphqlClient,
|
||||
account.id,
|
||||
new Date()
|
||||
);
|
||||
|
||||
const isFree = !billingUpdateQuery.billing; // TODO: no billing means free?
|
||||
const isMetered = !!billingUpdateQuery.billing?.subscription?.meteredBillingStatus?.EAS_UPDATE;
|
||||
// TODO: verify correctness
|
||||
if (!isFree || isMetered) {
|
||||
return;
|
||||
}
|
||||
|
||||
const planMetrics = billingUpdateQuery.usageMetrics.byBillingPeriod.planMetrics;
|
||||
for (const metric of planMetrics) {
|
||||
const { value, limit, serviceMetric } = metric;
|
||||
const percentageUsed = value / limit;
|
||||
if (percentageUsed >= 1) {
|
||||
Log.error(
|
||||
'You have exceeded your EAS Update plan limit. You can create updates but they will no longer be distributed to users.'
|
||||
);
|
||||
Log.error('Please upgrade your plan to continue receiving updates.');
|
||||
return;
|
||||
} else if (percentageUsed >= WARNING_THRESHOLD && percentageUsed < 1) {
|
||||
Log.warn(
|
||||
`You are approaching your EAS Update plan limit. You have used ${Math.round(
|
||||
percentageUsed * 100
|
||||
)}% of your plan's ${serviceMetric}.`
|
||||
);
|
||||
Log.warn('Please upgrade your plan to continue receiving updates.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,9 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
import { Response } from '../fetch';
|
||||
import { PartialManifest, PartialManifestAsset } from '../graphql/generated';
|
||||
import areSetsEqual from './expodash/areSetsEqual';
|
||||
|
||||
type CodeSigningInfo = {
|
||||
export type CodeSigningInfo = {
|
||||
privateKey: PKI.rsa.PrivateKey;
|
||||
certificate: PKI.Certificate;
|
||||
codeSigningMetadata: { alg: string; keyid: string };
|
||||
@@ -101,7 +102,7 @@ export async function getKeyAndCertificateFromPathsAsync({
|
||||
};
|
||||
}
|
||||
|
||||
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
|
||||
async function getMultipartBodyPartAsync(res: Response, partName: string): Promise<string | null> {
|
||||
const contentType = res.headers.get('content-type');
|
||||
if (!contentType) {
|
||||
throw new Error('The multipart manifest response is missing the content-type header');
|
||||
@@ -111,11 +112,19 @@ export async function getManifestBodyAsync(res: Response): Promise<string | null
|
||||
contentType,
|
||||
Buffer.from(bodyBuffer)
|
||||
);
|
||||
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, 'manifest'));
|
||||
const manifestPart = multipartParts.find(part => isMultipartPartWithName(part, partName));
|
||||
return manifestPart?.body ?? null;
|
||||
}
|
||||
|
||||
export function signManifestBody(body: string, codeSigningInfo: CodeSigningInfo): string {
|
||||
export async function getManifestBodyAsync(res: Response): Promise<string | null> {
|
||||
return await getMultipartBodyPartAsync(res, 'manifest');
|
||||
}
|
||||
|
||||
export async function getDirectiveBodyAsync(res: Response): Promise<string | null> {
|
||||
return await getMultipartBodyPartAsync(res, 'directive');
|
||||
}
|
||||
|
||||
export function signBody(body: string, codeSigningInfo: CodeSigningInfo): string {
|
||||
return signBufferRSASHA256AndVerify(
|
||||
codeSigningInfo.privateKey,
|
||||
codeSigningInfo.certificate,
|
||||
@@ -181,3 +190,20 @@ export function checkManifestBodyAgainstUpdateInfoGroup(
|
||||
assertAssetParity(correspondingManifestResponseBodyAssetJSON, nullthrows(partialManifestAsset));
|
||||
}
|
||||
}
|
||||
|
||||
export function checkDirectiveBodyAgainstUpdateInfoGroup(directiveResponseBody: string): void {
|
||||
const directiveResponseBodyJSON = JSON.parse(directiveResponseBody);
|
||||
|
||||
if (
|
||||
!areSetsEqual(
|
||||
new Set(Object.keys(directiveResponseBodyJSON)),
|
||||
new Set(['extra', 'type', 'parameters'])
|
||||
)
|
||||
) {
|
||||
throw new Error('Code signing directive integrity error: Unexpected keys');
|
||||
}
|
||||
|
||||
if (directiveResponseBodyJSON.type !== 'rollBackToEmbedded') {
|
||||
throw new Error('Code signing directive integrity error: Incorrect directive type');
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config-types';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import { boolish } from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
import resolveFrom, { silent as silentResolveFrom } from 'resolve-from';
|
||||
import semver from 'semver';
|
||||
|
||||
import Log from '../log';
|
||||
@@ -63,7 +63,8 @@ export async function expoCommandAsync(
|
||||
args: string[],
|
||||
{ silent = false }: { silent?: boolean } = {}
|
||||
): Promise<void> {
|
||||
const expoCliPath = resolveFrom(projectDir, 'expo/bin/cli.js');
|
||||
const expoCliPath =
|
||||
silentResolveFrom(projectDir, 'expo/bin/cli') ?? resolveFrom(projectDir, 'expo/bin/cli.js');
|
||||
const spawnPromise = spawnAsync(expoCliPath, args, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'], // inherit stdin so user can install a missing expo-cli from inside this command
|
||||
});
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import areSetsEqual from '../areSetsEqual';
|
||||
|
||||
describe(areSetsEqual, () => {
|
||||
it.each([
|
||||
[new Set([1, 2]), new Set([1, 2])],
|
||||
[new Set([1, 2]), new Set([2, 1])],
|
||||
])('equal cases: %p', (a, b) => {
|
||||
expect(areSetsEqual(a, b)).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[new Set([1, 2, 3]), new Set([1, 2])],
|
||||
[new Set([1, 2]), new Set([1, 2, 3])],
|
||||
])('non-equal cases: %p', (a, b) => {
|
||||
expect(areSetsEqual(a, b)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { truthy } from '../filter';
|
||||
import partition from '../partition';
|
||||
|
||||
describe(partition, () => {
|
||||
it('partitions', () => {
|
||||
expect(partition([true, false], truthy)).toMatchObject([[true], [false]]);
|
||||
|
||||
expect(partition([1, 2, 3, 4], e => e <= 2)).toMatchObject([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function areSetsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
return a.size === b.size && [...a].every(value => b.has(value));
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
export default function mapMap<K, V, M>(
|
||||
map: ReadonlyMap<K, V>,
|
||||
mapper: (value: V, key: K) => M
|
||||
): Map<K, M> {
|
||||
const resultingMap = new Map();
|
||||
for (const [k, v] of map) {
|
||||
resultingMap.set(k, mapper(v, k));
|
||||
}
|
||||
return resultingMap;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default async function mapMapAsync<K, V, M>(
|
||||
map: ReadonlyMap<K, V>,
|
||||
mapper: (value: V, key: K) => Promise<M>
|
||||
): Promise<Map<K, M>> {
|
||||
const resultingMap: Map<K, M> = new Map();
|
||||
await Promise.all(
|
||||
Array.from(map.keys()).map(async k => {
|
||||
const initialValue = map.get(k) as V;
|
||||
const result = await mapper(initialValue, k);
|
||||
resultingMap.set(k, result);
|
||||
})
|
||||
);
|
||||
return resultingMap;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
export default function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
||||
const trueItems: T[] = [];
|
||||
const falseItems: T[] = [];
|
||||
|
||||
for (const item of arr) {
|
||||
if (predicate(item)) {
|
||||
trueItems.push(item);
|
||||
} else {
|
||||
falseItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [trueItems, falseItems];
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
SubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
|
||||
import Log, { learnMore } from '../log';
|
||||
|
||||
type EasProfile<T extends ProfileType> = T extends 'build'
|
||||
? BuildProfile<Platform>
|
||||
: SubmitProfile<Platform>;
|
||||
@@ -57,11 +59,15 @@ async function readProfileAsync<T extends ProfileType>({
|
||||
profileName?: string;
|
||||
}): Promise<EasProfile<T>> {
|
||||
if (type === 'build') {
|
||||
return (await EasJsonUtils.getBuildProfileAsync(
|
||||
const buildProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
easJsonAccessor,
|
||||
platform,
|
||||
profileName
|
||||
)) as EasProfile<T>;
|
||||
);
|
||||
|
||||
maybePrintBuildProfileDeprecationWarnings(buildProfile, profileName);
|
||||
|
||||
return buildProfile as EasProfile<T>;
|
||||
} else {
|
||||
return (await EasJsonUtils.getSubmitProfileAsync(
|
||||
easJsonAccessor,
|
||||
@@ -70,3 +76,32 @@ async function readProfileAsync<T extends ProfileType>({
|
||||
)) as EasProfile<T>;
|
||||
}
|
||||
}
|
||||
|
||||
let hasPrintedDeprecationWarnings = false;
|
||||
|
||||
function maybePrintBuildProfileDeprecationWarnings(
|
||||
buildProfile: BuildProfile<Platform>,
|
||||
profileName?: string
|
||||
): void {
|
||||
if (hasPrintedDeprecationWarnings) {
|
||||
return;
|
||||
}
|
||||
const deprecationWarnings = EasJsonUtils.getBuildProfileDeprecationWarnings(
|
||||
buildProfile,
|
||||
profileName
|
||||
);
|
||||
if (deprecationWarnings.length === 0) {
|
||||
return;
|
||||
}
|
||||
Log.newLine();
|
||||
Log.warn('Detected deprecated fields in eas.json:');
|
||||
for (const warning of deprecationWarnings) {
|
||||
const warnlog: string = warning.message.map(line => `\t${line}`).join('\n');
|
||||
Log.warn(warnlog);
|
||||
if (warning.docsUrl) {
|
||||
Log.warn(`\t${learnMore(warning.docsUrl)}`);
|
||||
}
|
||||
Log.newLine();
|
||||
}
|
||||
hasPrintedDeprecationWarnings = true;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "3.6.1",
|
||||
"version": "3.9.2",
|
||||
"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": "1.0.7",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
|
||||
@@ -74,11 +74,6 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"cacheDefaultPaths": {
|
||||
"type": "boolean",
|
||||
"description": "Specifies whether to cache the recommended set of files, currently only Podfile.lock is cached by default for iOS build and nothing is cached for Android.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -354,7 +349,7 @@
|
||||
"default": "default",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["default", "medium", "m1-medium", "intel-medium"]
|
||||
"enum": ["default", "medium", "intel-medium", "m-medium"]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EasJsonAccessor patching JSON file 1`] = `
|
||||
exports[`fromProjectPath patching JSON file 1`] = `
|
||||
"{
|
||||
"build": {
|
||||
"production": {
|
||||
@@ -14,7 +14,7 @@ exports[`EasJsonAccessor patching JSON file 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`EasJsonAccessor patching JSON5 file (preserves comments) 1`] = `
|
||||
exports[`fromProjectPath patching JSON5 file (preserves comments) 1`] = `
|
||||
"{
|
||||
"build": {
|
||||
"production": {
|
||||
|
||||
@@ -15,7 +15,7 @@ beforeEach(async () => {
|
||||
vol.reset();
|
||||
});
|
||||
|
||||
describe(EasJsonAccessor, () => {
|
||||
describe(EasJsonAccessor.fromProjectPath, () => {
|
||||
test('patching JSON file', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
@@ -24,7 +24,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await accessor.readAsync();
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
@@ -44,7 +44,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await accessor.readAsync();
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
@@ -67,7 +67,7 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(
|
||||
/^Found invalid character in .*eas\.json.+/
|
||||
@@ -82,7 +82,57 @@ describe(EasJsonAccessor, () => {
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = new EasJsonAccessor(fakeAppPath);
|
||||
const accessor = EasJsonAccessor.fromProjectPath(fakeAppPath);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
|
||||
});
|
||||
});
|
||||
|
||||
describe(EasJsonAccessor.fromRawString, () => {
|
||||
test('patching JSON file', async () => {
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-json.json'), 'utf-8')
|
||||
);
|
||||
await accessor.readAsync();
|
||||
expect(() =>
|
||||
accessor.patch(o => {
|
||||
o.build.production.env.ABC = '456';
|
||||
return o;
|
||||
})
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test('reading invalid JSON5 object', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./eas.json': await fsReal.readFile(
|
||||
path.join(fixturesDir, 'eas-invalid-json5.json'),
|
||||
'utf-8'
|
||||
),
|
||||
},
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-invalid-json5.json'), 'utf-8')
|
||||
);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(
|
||||
/^Found invalid character in .*eas\.json.+/
|
||||
);
|
||||
});
|
||||
|
||||
test('reading empty JSON file', async () => {
|
||||
vol.fromJSON(
|
||||
{
|
||||
'./eas.json': await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8'),
|
||||
},
|
||||
fakeAppPath
|
||||
);
|
||||
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
await fsReal.readFile(path.join(fixturesDir, 'eas-empty.json'), 'utf-8')
|
||||
);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(accessor.readAsync()).rejects.toThrowError(/^.*eas\.json.* is empty\.$/g);
|
||||
});
|
||||
|
||||
@@ -20,7 +20,33 @@ test('minimal valid eas.json for both platforms', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
|
||||
expect(androidProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
|
||||
test('minimal valid eas.json for both platforms when reading eas.json from string', async () => {
|
||||
const accessor = EasJsonAccessor.fromRawString(
|
||||
JSON.stringify({
|
||||
build: {
|
||||
production: {},
|
||||
},
|
||||
})
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -52,7 +78,7 @@ test('valid eas.json for development client builds', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'debug');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -82,7 +108,7 @@ test(`valid eas.json with autoIncrement flag at build profile root`, async () =>
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -111,7 +137,7 @@ test('valid profile for internal distribution on Android', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'internal');
|
||||
expect(profile).toEqual({
|
||||
distribution: 'internal',
|
||||
@@ -133,7 +159,7 @@ test('valid profile extending other profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -176,7 +202,7 @@ test('valid profile extending other profile with platform specific envs', async
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -230,7 +256,6 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
},
|
||||
android: {
|
||||
cache: {
|
||||
cacheDefaultPaths: false,
|
||||
customPaths: ['somefakepath'],
|
||||
},
|
||||
},
|
||||
@@ -238,7 +263,7 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'base');
|
||||
const extendedAndroidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
@@ -261,7 +286,6 @@ test('valid profile extending other profile with platform specific caching', asy
|
||||
distribution: 'internal',
|
||||
credentialsSource: 'remote',
|
||||
cache: {
|
||||
cacheDefaultPaths: false,
|
||||
customPaths: ['somefakepath'],
|
||||
},
|
||||
});
|
||||
@@ -282,7 +306,7 @@ test('valid eas.json with missing profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'debug');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: debug');
|
||||
});
|
||||
@@ -294,7 +318,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(promise).rejects.toThrowError(
|
||||
@@ -305,7 +329,7 @@ test('invalid eas.json when using wrong buildType', async () => {
|
||||
test('empty json', async () => {
|
||||
await fs.writeJson('/project/eas.json', {});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError('Missing build profile in eas.json: production');
|
||||
});
|
||||
@@ -317,7 +341,7 @@ test('invalid semver value', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(InvalidEasJsonError);
|
||||
await expect(promise).rejects.toThrowError(
|
||||
@@ -332,7 +356,7 @@ test('invalid release channel', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const promise = EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production');
|
||||
await expect(promise).rejects.toThrowError(/fails to match the required pattern/);
|
||||
});
|
||||
@@ -345,7 +369,7 @@ test('get profile names', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const allProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(accessor);
|
||||
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
|
||||
});
|
||||
@@ -359,7 +383,7 @@ test('invalid resourceClass at build profile root', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
@@ -377,7 +401,7 @@ test('iOS-specific resourceClass', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).resolves.not.toThrow();
|
||||
@@ -394,8 +418,69 @@ test('Android-specific resourceClass', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production')
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('build profile with platform-specific custom build config', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
android: {
|
||||
config: 'production.android.yml',
|
||||
},
|
||||
ios: {
|
||||
config: 'production.ios.yml',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
expect(androidProfile).toEqual({
|
||||
config: 'production.android.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
expect(iosProfile).toEqual({
|
||||
config: 'production.ios.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
|
||||
test('build profiles with both platform build config', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
config: 'production.yml',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
expect(androidProfile).toEqual({
|
||||
config: 'production.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
expect(iosProfile).toEqual({
|
||||
config: 'production.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,7 @@ test('minimal allowed eas.json for both platforms', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
@@ -50,7 +50,7 @@ test('android config with all required values', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
@@ -80,7 +80,7 @@ test('android config with serviceAccountKeyPath set to env var', async () => {
|
||||
|
||||
try {
|
||||
process.env.GOOGLE_SERVICE_ACCOUNT = './path.json';
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const androidProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
@@ -114,7 +114,7 @@ test('ios config with all required values', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'production');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
@@ -148,7 +148,7 @@ test('ios config with ascApiKey fields set to env var', async () => {
|
||||
process.env.ASC_API_KEY_PATH = './path-ABCD.p8';
|
||||
process.env.ASC_API_KEY_ISSUER_ID = 'abc-123-def-456';
|
||||
process.env.ASC_API_KEY_ID = 'ABCD';
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'release');
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
@@ -189,7 +189,7 @@ test('valid profile extending other profile', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const baseProfile = await EasJsonUtils.getSubmitProfileAsync(accessor, Platform.IOS, 'base');
|
||||
const extendedProfile = await EasJsonUtils.getSubmitProfileAsync(
|
||||
accessor,
|
||||
@@ -233,7 +233,7 @@ test('get profile names', async () => {
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const allProfileNames = await EasJsonUtils.getSubmitProfileNamesAsync(accessor);
|
||||
expect(allProfileNames.sort()).toEqual(['production', 'blah'].sort());
|
||||
});
|
||||
@@ -38,7 +38,7 @@ const customErrorMessageHandlers: ((err: ValidationError) => void)[] = [
|
||||
];
|
||||
|
||||
export class EasJsonAccessor {
|
||||
private easJsonPath: string;
|
||||
private easJsonPath: string | undefined;
|
||||
|
||||
private isJson5 = false;
|
||||
private easJson: EasJson | undefined;
|
||||
@@ -46,8 +46,25 @@ export class EasJsonAccessor {
|
||||
private easJsonRawObject: any | undefined;
|
||||
private easJsonPatched: boolean = false;
|
||||
|
||||
constructor(projectDir: string) {
|
||||
this.easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
private constructor({ projectDir }: { projectDir: string });
|
||||
private constructor({ easJsonRawContents }: { easJsonRawContents: string });
|
||||
private constructor({
|
||||
projectDir,
|
||||
easJsonRawContents,
|
||||
}: {
|
||||
projectDir?: string;
|
||||
easJsonRawContents?: string;
|
||||
}) {
|
||||
this.easJsonPath = projectDir && EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||
this.easJsonRawContents = easJsonRawContents;
|
||||
}
|
||||
|
||||
public static fromProjectPath(projectDir: string): EasJsonAccessor {
|
||||
return new EasJsonAccessor({ projectDir });
|
||||
}
|
||||
|
||||
public static fromRawString(easJsonRawContents: string): EasJsonAccessor {
|
||||
return new EasJsonAccessor({ easJsonRawContents });
|
||||
}
|
||||
|
||||
public static formatEasJsonPath(projectDir: string): string {
|
||||
@@ -81,6 +98,9 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public async writeAsync(): Promise<void> {
|
||||
if (!this.easJsonPath) {
|
||||
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
|
||||
}
|
||||
if (!this.easJsonPatched) {
|
||||
return;
|
||||
}
|
||||
@@ -89,6 +109,9 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public patch(fn: (easJsonRawObject: any) => any): void {
|
||||
if (!this.easJsonPath) {
|
||||
throw new Error('Updates are not supported for EasJsonAccessor created from string.');
|
||||
}
|
||||
assert(
|
||||
this.easJsonRawContents && this.easJsonRawObject,
|
||||
'call readAsync/readRawJsonAsync first'
|
||||
@@ -104,16 +127,24 @@ export class EasJsonAccessor {
|
||||
}
|
||||
|
||||
public async readRawJsonAsync(): Promise<any> {
|
||||
if (!(await fs.pathExists(this.easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`${chalk.bold('eas.json')} could not be found at ${
|
||||
this.easJsonPath
|
||||
}. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
if (this.easJsonPath) {
|
||||
if (!(await fs.pathExists(this.easJsonPath))) {
|
||||
throw new MissingEasJsonError(
|
||||
`${chalk.bold('eas.json')} could not be found at ${
|
||||
this.easJsonPath
|
||||
}. Learn more at https://expo.fyi/eas-json`
|
||||
);
|
||||
}
|
||||
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
|
||||
}
|
||||
return this.parseRawJson();
|
||||
}
|
||||
|
||||
this.easJsonRawContents = await fs.readFile(this.easJsonPath, 'utf-8');
|
||||
|
||||
private parseRawJson(): any {
|
||||
assert(
|
||||
this.easJsonRawContents !== undefined,
|
||||
'easJsonRawContents needs to be set before calling parseRawJson'
|
||||
);
|
||||
if (this.easJsonRawContents.trim().length === 0) {
|
||||
throw new InvalidEasJsonError(`${chalk.bold('eas.json')} is empty.`);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { AndroidReleaseStatus, AndroidReleaseTrack, SubmitProfile } from './submit/types';
|
||||
export { getDefaultProfile as getDefaultSubmitProfile } from './submit/resolver';
|
||||
export { EasJson, ProfileType, AppVersionSource } from './types';
|
||||
export { Platform } from '@expo/eas-build-job';
|
||||
export {
|
||||
AndroidVersionAutoIncrement,
|
||||
BuildProfile,
|
||||
|
||||
@@ -13,7 +13,4 @@ export const EasJsonSchema = Joi.object({
|
||||
}),
|
||||
build: Joi.object().pattern(Joi.string(), BuildProfileSchema),
|
||||
submit: Joi.object().pattern(Joi.string(), SubmitProfileSchema),
|
||||
update: Joi.object({
|
||||
assetExcludePatterns: Joi.array().items(Joi.string()),
|
||||
}),
|
||||
});
|
||||
@@ -25,7 +25,4 @@ export interface EasJson {
|
||||
};
|
||||
build?: { [profileName: string]: EasJsonBuildProfile };
|
||||
submit?: { [profileName: string]: EasJsonSubmitProfile };
|
||||
update?: {
|
||||
assetExcludePatterns?: string[];
|
||||
};
|
||||
}
|
||||
@@ -8,6 +8,11 @@ import { resolveSubmitProfile } from './submit/resolver';
|
||||
import { SubmitProfile } from './submit/types';
|
||||
import { EasJson } from './types';
|
||||
|
||||
interface EasJsonDeprecationWarning {
|
||||
message: string[];
|
||||
docsUrl?: string;
|
||||
}
|
||||
|
||||
export class EasJsonUtils {
|
||||
public static async getBuildProfileNamesAsync(accessor: EasJsonAccessor): Promise<string[]> {
|
||||
const easJson = await accessor.readAsync();
|
||||
@@ -23,6 +28,26 @@ export class EasJsonUtils {
|
||||
return resolveBuildProfile({ easJson, platform, profileName });
|
||||
}
|
||||
|
||||
public static getBuildProfileDeprecationWarnings(
|
||||
buildProfile: BuildProfile,
|
||||
profileName?: string
|
||||
): EasJsonDeprecationWarning[] {
|
||||
const warnings: EasJsonDeprecationWarning[] = [];
|
||||
|
||||
if (buildProfile.cache?.cacheDefaultPaths !== undefined) {
|
||||
warnings.push({
|
||||
message: [
|
||||
`The "build.${
|
||||
profileName ?? 'production'
|
||||
}.cache.cacheDefaultPaths" field in eas.json is deprecated and will be removed in the future.`,
|
||||
],
|
||||
docsUrl: 'https://docs.expo.dev/build-reference/caching/#ios-dependencies',
|
||||
});
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public static async getCliConfigAsync(accessor: EasJsonAccessor): Promise<EasJson['cli'] | null> {
|
||||
try {
|
||||
const easJson = await accessor.readAsync();
|
||||
@@ -48,18 +73,4 @@ export class EasJsonUtils {
|
||||
const easJson = await accessor.readAsync();
|
||||
return resolveSubmitProfile({ easJson, platform, profileName });
|
||||
}
|
||||
|
||||
public static async getUpdatesConfigAsync(
|
||||
accessor: EasJsonAccessor
|
||||
): Promise<EasJson['update'] | null> {
|
||||
try {
|
||||
const easJson = await accessor.readAsync();
|
||||
return easJson.update ?? null;
|
||||
} catch (err: any) {
|
||||
if (err instanceof MissingEasJsonError) {
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ set -eo pipefail
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/../.. && pwd )"
|
||||
LOCAL_TS_PATH="$ROOT_DIR/packages/eas-cli/src/build/local.ts"
|
||||
|
||||
LOCAL_BUILD_PLUGIN_NPM_TAG="eas-cli"
|
||||
|
||||
echo "Checking if eas-cli-local-build-plugin upgrade is available"
|
||||
|
||||
plugin_version_latest=$(npm info eas-cli-local-build-plugin@latest version)
|
||||
plugin_version_latest=$(npm info eas-cli-local-build-plugin@$LOCAL_BUILD_PLUGIN_NPM_TAG version)
|
||||
plugin_version_current=$(grep 'const PLUGIN_PACKAGE_VERSION' $LOCAL_TS_PATH | cut -d"'" -f2)
|
||||
|
||||
if [[ "$plugin_version_latest" == "$plugin_version_current" ]]; then
|
||||
|
||||
@@ -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.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/apple-utils/-/apple-utils-1.2.0.tgz#227d118089a4645b076162f3b513dfc68763ca21"
|
||||
integrity sha512-MotvQqu8CNVlnl9nsISobXDjEzSMfT7U5Zk+1rhOFoJhaZhlEkARxumUFUgH6cYnK7J2mMd3YxM+G769arkEKQ==
|
||||
|
||||
"@expo/bunyan@^4.0.0":
|
||||
version "4.0.0"
|
||||
@@ -1390,21 +1390,13 @@
|
||||
slugify "^1.3.4"
|
||||
sucrase "^3.20.0"
|
||||
|
||||
"@expo/eas-build-job@0.2.102":
|
||||
version "0.2.102"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-0.2.102.tgz#6c23388a7ac12d60a46d2a9ef779075f5c1a874b"
|
||||
integrity sha512-1gav4gHhZCnRDIscd0/sat/fjts543QmexiWXUWDv15uzCxx/NnewN13tcmOwMV0K3Z3agOlJLRPN2t6UvIMAg==
|
||||
"@expo/eas-build-job@1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.7.tgz#4c50dbc37faf000c5a5d791e69995bbf0eda39ff"
|
||||
integrity sha512-LBYXWS/wp6YnyNDUqt+5+k0J7P6gf3/hjf48yJp+BcLabQbqWqAuz7eqPMEDt9bezMX2fEAiYywSBdnRJKo3Ow==
|
||||
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"
|
||||
@@ -1441,6 +1433,13 @@
|
||||
json5 "^2.2.2"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/logger/-/logger-1.0.0.tgz#41f17209e11aa83eabd5722c0efbc5b80efc7c41"
|
||||
integrity sha512-i07o2xUsmtpC2mQ3ZHOJRtFEqWTfQabOb3U5Q0Hwip/BvI0yj8Pweu2ExhKLERbGugt4tSSLdQJUcKQbOrue6g==
|
||||
dependencies:
|
||||
"@expo/bunyan" "^4.0.0"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/multipart-body-parser/-/multipart-body-parser-1.1.0.tgz#ba402b3047cf14ef17b4d785ec030dd6474ae6e4"
|
||||
@@ -1573,6 +1572,26 @@
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
|
||||
"@expo/spawn-async@^1.7.0":
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@expo/spawn-async/-/spawn-async-1.7.2.tgz#fcfe66c3e387245e72154b1a7eae8cada6a47f58"
|
||||
integrity sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==
|
||||
dependencies:
|
||||
cross-spawn "^7.0.3"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@expo/steps/-/steps-1.0.8.tgz#06fe9ca5bde449255661cf090666e70dd2ae5345"
|
||||
integrity sha512-1SxHOtfLwCdKxOUtbXjz548JQlkkVoNjaydNmo7zZgPvD/P4aviaOSCL3StUpQuWNlcERLzjOn8/NgvsdDmMyw==
|
||||
dependencies:
|
||||
"@expo/logger" "1.0.0"
|
||||
"@expo/spawn-async" "^1.7.0"
|
||||
arg "^5.0.2"
|
||||
joi "^17.7.0"
|
||||
this-file "^2.0.3"
|
||||
uuid "^9.0.0"
|
||||
yaml "^2.2.1"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/timeago.js/-/timeago.js-1.0.0.tgz#8fb2b17e93e7a8d28387a4d292af12e071040045"
|
||||
@@ -3985,13 +4004,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 +4011,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"
|
||||
@@ -4847,6 +4859,11 @@ arg@^4.1.0:
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
arg@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
|
||||
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@@ -9328,15 +9345,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:
|
||||
@@ -12824,6 +12841,11 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
this-file@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/this-file/-/this-file-2.0.3.tgz#13bd2bcfbab2ce86a37a15689df1d14861032b1b"
|
||||
integrity sha512-IdMH1bUkVJdJjM7o8v83Mv4QvVPdkAofur20STl2Bbw9uMuuS/bT/PZURkEdZsy9XC/1ZXWgZ1wIL9nvouGaEg==
|
||||
|
||||
through2@^2.0.0:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||
@@ -13337,7 +13359,7 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c"
|
||||
integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==
|
||||
|
||||
[email protected]:
|
||||
[email protected], uuid@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==
|
||||
@@ -13777,6 +13799,11 @@ yaml@^1.10.0, yaml@^1.7.2:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e"
|
||||
integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==
|
||||
|
||||
yaml@^2.2.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.2.1.tgz#3014bf0482dcd15147aa8e56109ce8632cd60ce4"
|
||||
integrity sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==
|
||||
|
||||
[email protected], yargs-parser@^20.2.2:
|
||||
version "20.2.4"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54"
|
||||
|
||||
Reference in new issue
Block a user