mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
88
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 | ||
|
|
3ad644bb9d | ||
|
|
233e5451a8 | ||
|
|
ce954cdecc | ||
|
|
59bd1caf98 | ||
|
|
86877fdda3 | ||
|
|
9299c9b7cc | ||
|
|
64fb2e5b3a | ||
|
|
91fdbac773 | ||
|
|
507eadb914 | ||
|
|
f1cde7a291 | ||
|
|
8b9ad47d16 | ||
|
|
192984b47d | ||
|
|
2c36810ce7 | ||
|
|
bfa63f95fe | ||
|
|
c414b9f975 | ||
|
|
6946a085cb | ||
|
|
317d2fcbb6 | ||
|
|
dd61b4bb70 | ||
|
|
80fbf7cf62 | ||
|
|
26b6a17160 | ||
|
|
7f4e2eef95 | ||
|
|
88b683655c | ||
|
|
f3aa3d98d6 | ||
|
|
f852b9329d | ||
|
|
a2340531b0 | ||
|
|
39df3a1364 | ||
|
|
2f1d454582 | ||
|
|
e2560ea867 | ||
|
|
346d136b8b | ||
|
|
dd77312510 | ||
|
|
120a965368 | ||
|
|
050336b6fd | ||
|
|
7fc3e3fcdb | ||
|
|
2d117e5b4a | ||
|
|
131377bd2c | ||
|
|
ed61b0c41b | ||
|
|
9f4d3bfc56 |
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==
|
||||
@@ -161,6 +161,8 @@ jobs:
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
@@ -197,9 +199,9 @@ jobs:
|
||||
env:
|
||||
SLACK_CHANNEL: eas-cli
|
||||
SLACK_COLOR: ${{ job.status }}
|
||||
SLACK_ICON: https://github.com/expo-ci.png?size=48
|
||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
||||
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
||||
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
||||
SLACK_USERNAME: 'Expo CI'
|
||||
SLACK_USERNAME: 'EAS CLI CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
@@ -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 }}
|
||||
+141
-1
@@ -10,10 +10,150 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix incorrect exit code for successful local builds. ([#1649](https://github.com/expo/eas-cli/pull/1649) by [@dsokal](https://github.com/dsokal))
|
||||
### 🧹 Chores
|
||||
|
||||
## [3.9.3](https://github.com/expo/eas-cli/releases/tag/v3.9.3) - 2023-04-18
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix prompts in `eas update`. ([#1797](https://github.com/expo/eas-cli/pull/1797) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
## [3.9.2](https://github.com/expo/eas-cli/releases/tag/v3.9.2) - 2023-04-13
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fixes the rollout percentages when ending a rollout. ([#1781](https://github.com/expo/eas-cli/pull/1781) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Print meaningful error message if `extra.eas.projectId` is not a string. ([#1788](https://github.com/expo/eas-cli/pull/1788) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Show output of `expo install expo-updates`. ([#1782](https://github.com/expo/eas-cli/pull/1782) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Print deprecation warnings for deprecated `eas.json` fields. ([#1768](https://github.com/expo/eas-cli/pull/1768) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.1](https://github.com/expo/eas-cli/releases/tag/v3.9.1) - 2023-04-10
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update apple utilities to work around the maintenance error. ([#1780](https://github.com/expo/eas-cli/pull/1780) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Don't use defaults for the `cache.cacheDefaultPaths` build profile field. ([#1769](https://github.com/expo/eas-cli/pull/1769) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.9.0](https://github.com/expo/eas-cli/releases/tag/v3.9.0) - 2023-04-06
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add support for roll back to embedded updates. ([#1754](https://github.com/expo/eas-cli/pull/1754), [#1755](https://github.com/expo/eas-cli/pull/1755) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Update `expoCommandAsync` to support the new Expo CLI bin path. ([#1772](https://github.com/expo/eas-cli/pull/1772) by [@gabrieldonadel](https://github.com/gabrieldonadel))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Use the `eas-cli` npm tag for checking for the local build plugin updates. ([#1759](https://github.com/expo/eas-cli/pull/1759) by [@dsokal](https://github.com/dsokal))
|
||||
- Ignore `requireCommit` option in `eas build:internal`. ([#1760](https://github.com/expo/eas-cli/pull/1760) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Support parsing eas.json from a string. ([#1766](https://github.com/expo/eas-cli/pull/1766) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.8.1](https://github.com/expo/eas-cli/releases/tag/v3.8.1) - 2023-03-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
|
||||
|
||||
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Disable analytics when running behind a proxy. ([#1696](https://github.com/expo/eas-cli/pull/1696) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Clarify missing owner field error message for Robot users. ([#1702](https://github.com/expo/eas-cli/pull/1702) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Validate the platform for local builds earlier. ([#1698](https://github.com/expo/eas-cli/pull/1698) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Display build message when picking build in `eas submit`, `eas build:resign`, and `eas build:run` commands. ([#1700](https://github.com/expo/eas-cli/pull/1700) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add a link directly to a build phase logs in the EAS CLI build error message. ([#1699](https://github.com/expo/eas-cli/pull/1699) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.6.0](https://github.com/expo/eas-cli/releases/tag/v3.6.0) - 2023-02-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use `sdkVersion` as default runtime version policy when running `eas update:configure`. ([#1669](https://github.com/expo/eas-cli/pull/1669) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Warn when project ID but no owner specified and mismatch logged in user. ([#1667](https://github.com/expo/eas-cli/pull/1667) by [@wschurman](https://github.com/wschurman))
|
||||
- Return the build `message` if one is available. ([#1691](https://github.com/expo/eas-cli/pull/1691) by [@raulriera](https://github.com/raulriera))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix suggested application identifier to match owning account name. ([#1670](https://github.com/expo/eas-cli/pull/1670) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add Xcode 14.2 image to the VSCode schema. ([#1693](https://github.com/expo/eas-cli/pull/1693) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.5.2](https://github.com/expo/eas-cli/releases/tag/v3.5.2) - 2023-02-05
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use the new developer-mdn.apple.com subdomain instead of developer.apple.com. ([#1673](https://github.com/expo/eas-cli/pull/1673) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [3.5.1](https://github.com/expo/eas-cli/releases/tag/v3.5.1) - 2023-02-01
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue with the `eas.json` envs being not available when resolving dynamic config. ([#1666](https://github.com/expo/eas-cli/pull/1666) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.5.0](https://github.com/expo/eas-cli/releases/tag/v3.5.0) - 2023-01-31
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Set `m1-medium` resource class for SDK version `>=48` and RN version `>=0.71` builds with unspecified resource class. ([#1655](https://github.com/expo/eas-cli/pull/1655) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix undefined branchMappingLogic. ([#1653](https://github.com/expo/eas-cli/pull/1653) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Fix using local credentials for internal distribution builds with universal provisioning. ([#1657](https://github.com/expo/eas-cli/pull/1657) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Move image validation to the server side and better handle server validation errors in the eas-cli. ([#1650](https://github.com/expo/eas-cli/pull/1650) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Update metadata on the worker when using git-based builds. ([#1651](https://github.com/expo/eas-cli/pull/1651) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.4.1](https://github.com/expo/eas-cli/releases/tag/v3.4.1) - 2023-01-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix incorrect exit code for successful local builds. ([#1649](https://github.com/expo/eas-cli/pull/1649) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [3.4.0](https://github.com/expo/eas-cli/releases/tag/v3.4.0) - 2023-01-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.4.1",
|
||||
"version": "3.9.3",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
"eas": "packages/eas-cli/bin/run",
|
||||
"lint": "eslint .",
|
||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
||||
"release": "./scripts/bin/run release",
|
||||
"release": "lerna run rebuild && ./scripts/bin/run release",
|
||||
"test": "jest",
|
||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||
},
|
||||
|
||||
+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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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.4.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
+4096
-709
File diff suppressed because it is too large.
Load diff
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.4.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.34",
|
||||
"@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.102",
|
||||
"@expo/eas-json": "3.3.2",
|
||||
"@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",
|
||||
@@ -202,7 +203,8 @@
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rimraf oclif.manifest.json",
|
||||
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
|
||||
@@ -437,8 +437,8 @@
|
||||
"$ref": "#/definitions/apple/AppleKidsAge"
|
||||
}
|
||||
],
|
||||
"description": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n@see https://developer.apple.com/news/?id=091202019a",
|
||||
"markdownDescription": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n\n[Learn more](https://developer.apple.com/news/?id=091202019a)"
|
||||
"description": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n@see https://developer-mdn.apple.com/news/?id=091202019a",
|
||||
"markdownDescription": "When parents visit the Kids category on the App Store, they expect the apps they find will protect their children’s data, provide only age-appropriate content, and require a parental gate in order to link out of the app, request permissions, or present purchasing opportunities. It’s critical that no personally identifiable information or device information be transmitted to third parties, and that advertisements are human-reviewed for age appropriateness in order to be displayed.\n\n[Learn more](https://developer-mdn.apple.com/news/?id=091202019a)"
|
||||
},
|
||||
"seventeenPlus": {
|
||||
"type": "boolean",
|
||||
|
||||
@@ -133,7 +133,9 @@ export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration
|
||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
|
||||
}
|
||||
|
||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true);
|
||||
const analyticsEnabled =
|
||||
!process.env.https_proxy && // disable analytics if running behind proxy
|
||||
(await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, true));
|
||||
if (!analyticsEnabled) {
|
||||
return new NoOpAnalytics();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
|
||||
import { EasCommandError } from '../../commandUtils/errors';
|
||||
import Build from '../../commands/build';
|
||||
import Log, { link } from '../../log';
|
||||
import { handleBuildRequestError } from '../build';
|
||||
import {
|
||||
EasBuildDownForMaintenanceError,
|
||||
EasBuildFreeTierDisabledAndroidError,
|
||||
EasBuildFreeTierDisabledError,
|
||||
EasBuildFreeTierDisabledIOSError,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
RequestValidationError,
|
||||
TurtleDeprecatedJobFormatError,
|
||||
} from '../errors';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
|
||||
{
|
||||
"message": "Error 1"
|
||||
},
|
||||
{
|
||||
"message": "Error 2"
|
||||
},
|
||||
{
|
||||
"message": "Error 3"
|
||||
}
|
||||
]`;
|
||||
|
||||
const EXPECTED_EAS_BUILD_DOWN_MESSAGE = `EAS Build is down for maintenance. Try again later. Check ${link(
|
||||
'https://status.expo.dev/'
|
||||
)} for updates.`;
|
||||
|
||||
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID = `You have already reached the maximum number of pending Android builds for your account. Try again later.`;
|
||||
const EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS = `You have already reached the maximum number of pending iOS builds for your account. Try again later.`;
|
||||
|
||||
const EXPECTED_GENERIC_MESSAGE =
|
||||
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.';
|
||||
|
||||
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
|
||||
return new GraphQLError(message, null, null, null, null, null, {
|
||||
errorCode,
|
||||
});
|
||||
};
|
||||
|
||||
const assertReThrownError = (
|
||||
caughtError: CombinedError | Error,
|
||||
expectedType: typeof EasCommandError,
|
||||
expectedMessage: string = 'Error 1'
|
||||
): void => {
|
||||
expect(caughtError).toBeInstanceOf(expectedType);
|
||||
expect((caughtError as Error).message).toEqual(expectedMessage);
|
||||
};
|
||||
|
||||
describe(Build.name, () => {
|
||||
describe('handle build request errors', () => {
|
||||
describe('logs graphQL errors to debug if present', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const graphQLErrors = ['Error 1', 'Error 2', 'Error 3'];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON);
|
||||
});
|
||||
});
|
||||
|
||||
describe('logs nothing to debug if graphQLErrors not present', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const error = new Error('Generic error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(undefined);
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const logDebugSpy = jest.spyOn(Log, 'debug');
|
||||
const error = new Error('Generic error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch {}
|
||||
|
||||
expect(logDebugSpy).toBeCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is server-side defined error', () => {
|
||||
describe('throws correct EasCommandError subclass', () => {
|
||||
describe('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, TurtleDeprecatedJobFormatError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('does it for EAS_BUILD_FREE_TIER_DISABLED_IOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_IOS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledIOSError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, EasBuildFreeTierDisabledAndroidError);
|
||||
}
|
||||
});
|
||||
|
||||
describe('for VALIDATION_ERROR', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, RequestValidationError);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, RequestValidationError);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is other expected graphQL-related error', () => {
|
||||
describe('throws correct EasCommandError subclass', () => {
|
||||
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildDownForMaintenanceError,
|
||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildDownForMaintenanceError,
|
||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(
|
||||
caughtError as Error,
|
||||
EasBuildTooManyPendingBuildsError,
|
||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is unexpected graphQL-related error', () => {
|
||||
describe('throws base Error class with custom message', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
||||
const graphQLErrors = [graphQLError];
|
||||
const error = new CombinedError({ graphQLErrors });
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
assertReThrownError(caughtError as Error, Error, EXPECTED_GENERIC_MESSAGE);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error is unexpected non-graphQL-related error', () => {
|
||||
describe('throws base Error class with propagated message', () => {
|
||||
it('does it with Android', async () => {
|
||||
const platform = Platform.ANDROID;
|
||||
const error = new Error('Non-graphQL-related error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toStrictEqual(error);
|
||||
}
|
||||
});
|
||||
|
||||
it('does it with iOS', async () => {
|
||||
const platform = Platform.IOS;
|
||||
const error = new Error('Non-graphQL-related error');
|
||||
|
||||
try {
|
||||
handleBuildRequestError(error, platform);
|
||||
} catch (caughtError) {
|
||||
expect(caughtError).toStrictEqual(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -62,7 +62,7 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
|
||||
}
|
||||
|
||||
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||
@@ -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,11 +1,17 @@
|
||||
import { Android } from '@expo/eas-build-job';
|
||||
|
||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
import {
|
||||
transformBuildMode,
|
||||
transformBuildTrigger,
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
@@ -21,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,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
BuildStatus,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore, link } from '../log';
|
||||
import { Ora, ora } from '../ora';
|
||||
@@ -36,6 +37,16 @@ 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';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
@@ -142,6 +153,10 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
|
||||
buildId: nullthrows(process.env.EAS_BUILD_ID),
|
||||
metadata: transformMetadata(metadata),
|
||||
});
|
||||
printJsonOnlyOutput({ job, metadata });
|
||||
return undefined;
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
@@ -156,37 +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',
|
||||
];
|
||||
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.'
|
||||
);
|
||||
}
|
||||
@@ -601,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);
|
||||
@@ -609,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
|
||||
import JsonFile from '@expo/json-file';
|
||||
import getenv from 'getenv';
|
||||
import resolveFrom from 'resolve-from';
|
||||
@@ -9,7 +9,6 @@ import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/An
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { CredentialsContext } from '../credentials/context';
|
||||
import { BuildResourceClass } from '../graphql/generated';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { resolveWorkflowAsync } from '../project/workflow';
|
||||
import { Actor } from '../user/User';
|
||||
@@ -17,6 +16,7 @@ import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
|
||||
|
||||
export async function createBuildContextAsync<T extends Platform>({
|
||||
buildProfileName,
|
||||
@@ -28,7 +28,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
noWait,
|
||||
platform,
|
||||
projectDir,
|
||||
resourceClass,
|
||||
resourceClassFlag,
|
||||
message,
|
||||
actor,
|
||||
graphqlClient,
|
||||
@@ -44,7 +44,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
noWait: boolean;
|
||||
platform: T;
|
||||
projectDir: string;
|
||||
resourceClass: BuildResourceClass;
|
||||
resourceClassFlag?: ResourceClass;
|
||||
message?: string;
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
@@ -52,7 +52,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
getDynamicProjectConfigAsync: DynamicConfigContextFn;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicProjectConfigAsync({ env: buildProfile.env });
|
||||
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||
@@ -88,6 +87,14 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
};
|
||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
||||
|
||||
const resourceClass = await resolveBuildResourceClassAsync(
|
||||
buildProfile,
|
||||
platform,
|
||||
projectDir,
|
||||
exp,
|
||||
resourceClassFlag
|
||||
);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountName: account.name,
|
||||
buildProfile,
|
||||
|
||||
@@ -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 {}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
BuildTrigger,
|
||||
Metadata,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
@@ -7,6 +13,7 @@ import {
|
||||
BuildMode,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
BuildTrigger as GraphQLBuildTrigger,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
@@ -84,10 +91,24 @@ 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}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildTrigger {
|
||||
if (buildTrigger === BuildTrigger.EAS_CLI) {
|
||||
return GraphQLBuildTrigger.EasCli;
|
||||
} else if (buildTrigger === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
return GraphQLBuildTrigger.GitBasedIntegration;
|
||||
}
|
||||
throw new Error('Unknown build trigger');
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export async function createIosContextAsync(
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp, ctx.user);
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
|
||||
@@ -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,16 +2,22 @@ import { Ios } from '@expo/eas-build-job';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||
import {
|
||||
transformBuildMode,
|
||||
transformBuildTrigger,
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
type: transformWorkflow(job.type),
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: nullthrows(job.projectRootDirectory),
|
||||
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,
|
||||
@@ -23,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';
|
||||
@@ -37,7 +37,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
@@ -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);
|
||||
}
|
||||
@@ -155,11 +156,9 @@ async function getNativeChannelAsync<T extends Platform>(
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function getReactNativeVersionAsync(
|
||||
ctx: BuildContext<Platform>
|
||||
): Promise<string | undefined> {
|
||||
export async function getReactNativeVersionAsync(projectDir: string): Promise<string | undefined> {
|
||||
try {
|
||||
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
|
||||
const reactNativePackageJsonPath = resolveFrom(projectDir, 'react-native/package.json');
|
||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||
} catch (err) {
|
||||
Log.debug('Failed to resolve react-native version:');
|
||||
|
||||
@@ -99,10 +99,6 @@ export async function listAndSelectBuildOnAppAsync(
|
||||
}
|
||||
}
|
||||
|
||||
function formatBuildChoiceValue(value: string | undefined | null): string {
|
||||
return value ? chalk.bold(value) : 'Unknown';
|
||||
}
|
||||
|
||||
function createBuildToPartialChoiceMaker(
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
|
||||
) {
|
||||
@@ -119,19 +115,34 @@ function createBuildToPartialChoiceMaker(
|
||||
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
|
||||
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
|
||||
)}"`
|
||||
: 'Unknown';
|
||||
: null;
|
||||
|
||||
const descriptionItems: { name: string; value: string | null }[] = [
|
||||
{ name: 'Version', value: build.appVersion ? chalk.bold(build.appVersion) : null },
|
||||
{ name: 'Commit', value: formattedCommitData },
|
||||
{
|
||||
name: 'Message',
|
||||
value: build.message
|
||||
? chalk.bold(
|
||||
build.message.length > 200 ? `${build.message.slice(0, 200)}...` : build.message
|
||||
)
|
||||
: null,
|
||||
},
|
||||
{
|
||||
name: build.platform === AppPlatform.Ios ? 'Build number' : 'Version code',
|
||||
value: build.appBuildVersion ? chalk.bold(build.appBuildVersion) : null,
|
||||
},
|
||||
];
|
||||
|
||||
const filteredDescriptionArray: string[] = descriptionItems
|
||||
.filter(item => item.value)
|
||||
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
|
||||
|
||||
return {
|
||||
title: `${chalk.bold(`ID:`)} ${build.id} (${chalk.bold(
|
||||
`${fromNow(new Date(build.completedAt))} ago`
|
||||
)})`,
|
||||
description: [
|
||||
`\t${chalk.bold(`Version:`)} ${formatBuildChoiceValue(build.appVersion)}`,
|
||||
`\t${chalk.bold(
|
||||
build.platform === AppPlatform.Ios ? 'Build number:' : 'Version code:'
|
||||
)} ${formatBuildChoiceValue(build.appBuildVersion)}`,
|
||||
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
|
||||
].join('\n'),
|
||||
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
|
||||
disabled: selectPromptDisabledFunction?.(build),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -18,7 +18,6 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildResourceClass,
|
||||
BuildStatus,
|
||||
BuildWithSubmissionsFragment,
|
||||
SubmissionFragment,
|
||||
@@ -32,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';
|
||||
@@ -84,81 +84,6 @@ export interface BuildFlags {
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
|
||||
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosM1Medium,
|
||||
[ResourceClass.M1_LARGE]: BuildResourceClass.IosM1Large,
|
||||
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
|
||||
};
|
||||
|
||||
const androidResourceClassToBuildResourceClassMapping: Record<
|
||||
Exclude<
|
||||
ResourceClass,
|
||||
| ResourceClass.M1_EXPERIMENTAL
|
||||
| ResourceClass.M1_MEDIUM
|
||||
| ResourceClass.M1_LARGE
|
||||
| ResourceClass.INTEL_MEDIUM
|
||||
>,
|
||||
BuildResourceClass
|
||||
> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
|
||||
};
|
||||
|
||||
function resolveBuildResourceClass(
|
||||
profile: ProfileData<'build'>,
|
||||
resourceClassFlag?: ResourceClass
|
||||
): BuildResourceClass {
|
||||
if (
|
||||
profile.platform !== Platform.IOS &&
|
||||
resourceClassFlag &&
|
||||
[
|
||||
ResourceClass.M1_EXPERIMENTAL,
|
||||
ResourceClass.M1_MEDIUM,
|
||||
ResourceClass.M1_LARGE,
|
||||
ResourceClass.INTEL_MEDIUM,
|
||||
].includes(resourceClassFlag)
|
||||
) {
|
||||
throw new Error(`Resource class ${resourceClassFlag} is only available for iOS builds`);
|
||||
}
|
||||
|
||||
const profileResourceClass = profile.profile.resourceClass;
|
||||
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
|
||||
Log.warn(
|
||||
`Build profile specifies the "${profileResourceClass}" resource class but you passed "${resourceClassFlag}" to --resource-class.\nUsing the "${resourceClassFlag}" as the override.`
|
||||
);
|
||||
}
|
||||
const resourceClass = resourceClassFlag ?? profileResourceClass ?? ResourceClass.DEFAULT;
|
||||
|
||||
if (profile.platform === Platform.IOS && resourceClass === ResourceClass.M1_EXPERIMENTAL) {
|
||||
Log.warn(`Resource class ${chalk.bold('m1-experimental')} is deprecated.`);
|
||||
}
|
||||
if (
|
||||
profile.platform === Platform.IOS &&
|
||||
[ResourceClass.LARGE, ResourceClass.M1_LARGE].includes(resourceClass)
|
||||
) {
|
||||
Log.warn(
|
||||
`Large resource classes are not available for iOS builds yet. Your build will use the medium resource class.`
|
||||
);
|
||||
}
|
||||
|
||||
return profile.platform === Platform.ANDROID
|
||||
? androidResourceClassToBuildResourceClassMapping[
|
||||
resourceClass as Exclude<
|
||||
ResourceClass,
|
||||
| ResourceClass.M1_EXPERIMENTAL
|
||||
| ResourceClass.M1_MEDIUM
|
||||
| ResourceClass.M1_LARGE
|
||||
| ResourceClass.INTEL_MEDIUM
|
||||
>
|
||||
]
|
||||
: iosResourceClassToBuildResourceClassMapping[resourceClass];
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
@@ -174,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)) ?? {};
|
||||
|
||||
@@ -194,6 +119,7 @@ export async function runBuildAndSubmitAsync(
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
|
||||
}
|
||||
|
||||
const startedBuilds: {
|
||||
@@ -208,7 +134,6 @@ export async function runBuildAndSubmitAsync(
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
resourceClass: resolveBuildResourceClass(buildProfile, flags.resourceClass),
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
@@ -316,7 +241,6 @@ async function prepareAndStartBuildAsync({
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
resourceClass,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
@@ -327,7 +251,6 @@ async function prepareAndStartBuildAsync({
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
resourceClass: BuildResourceClass;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
@@ -336,7 +259,7 @@ async function prepareAndStartBuildAsync({
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
resourceClass,
|
||||
resourceClassFlag: flags.resourceClass,
|
||||
clearCache: flags.clearCache,
|
||||
buildProfile: buildProfile.profile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
|
||||
@@ -38,6 +38,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
value: build.message,
|
||||
},
|
||||
{
|
||||
label: 'Distribution',
|
||||
value: build.distribution?.toLowerCase(),
|
||||
|
||||
@@ -15,6 +15,32 @@ import Log, { learnMore, link } from '../../log';
|
||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
||||
|
||||
function terminalLinkFallback(url: string, text: string): string {
|
||||
return `${text} (${url})`;
|
||||
}
|
||||
|
||||
const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) => string> = {
|
||||
EAS_BUILD_UNKNOWN_FASTLANE_ERROR: build =>
|
||||
`The ${link(getBuildLogsUrl(build, 'run-fastlane'), {
|
||||
text: '"Run fastlane"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-fastlane'), '"Run fastlane"'),
|
||||
})} step failed with an unknown error. Refer to the ${link(
|
||||
getBuildLogsUrl(build, 'xcode-logs'),
|
||||
{
|
||||
text: '"Xcode logs"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'xcode-logs'), '"Xcode logs"'),
|
||||
}
|
||||
)} phase for additional, more detailed logs`,
|
||||
EAS_BUILD_UNKNOWN_GRADLE_ERROR: build =>
|
||||
`Gradle build failed with unknown error. See logs for the ${link(
|
||||
getBuildLogsUrl(build, 'run-gradlew'),
|
||||
{
|
||||
text: '"Run gradlew"',
|
||||
fallback: terminalLinkFallback(getBuildLogsUrl(build, 'run-gradlew'), '"Run gradlew"'),
|
||||
}
|
||||
)} phase for more information.`,
|
||||
};
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
|
||||
@@ -31,7 +57,6 @@ export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
}
|
||||
|
||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
Log.newLine();
|
||||
if (builds.length === 1) {
|
||||
const [build] = builds;
|
||||
assert(build, 'Build should be defined');
|
||||
@@ -42,8 +67,8 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
}
|
||||
|
||||
function printBuildResult(build: BuildFragment): void {
|
||||
Log.addNewLineIfNone();
|
||||
if (build.status === BuildStatus.Errored) {
|
||||
Log.addNewLineIfNone();
|
||||
const userError = build.error;
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
@@ -51,11 +76,12 @@ function printBuildResult(build: BuildFragment): void {
|
||||
} build failed${userError ? ':' : ''}`
|
||||
);
|
||||
if (userError) {
|
||||
printUserError(userError);
|
||||
printUserError(userError, build);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (build.status === BuildStatus.Canceled) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.error(
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
@@ -65,6 +91,7 @@ function printBuildResult(build: BuildFragment): void {
|
||||
}
|
||||
|
||||
if (build.distribution === DistributionType.Internal) {
|
||||
Log.addNewLineIfNone();
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
// It's tricky to install the .apk file directly on Android so let's fallback
|
||||
// to the build details page and let people press the button to download there
|
||||
@@ -82,9 +109,14 @@ function printBuildResult(build: BuildFragment): void {
|
||||
} else {
|
||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
||||
// we should account for this case better if it is possible
|
||||
const url = build.artifacts?.buildUrl ?? '';
|
||||
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||
Log.log(`${link(url)}`);
|
||||
const url = build.artifacts?.buildUrl;
|
||||
if (url) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
|
||||
);
|
||||
Log.log(link(url));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +137,22 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
|
||||
}
|
||||
}
|
||||
|
||||
export function printUserError(error: BuildError): void {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
export function printUserError(error: BuildError, build: BuildFragment): void {
|
||||
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build);
|
||||
if (maybeErrorMessageOverride) {
|
||||
Log.error(maybeErrorMessageOverride);
|
||||
} else {
|
||||
Log.error(error.message);
|
||||
if (error.docsUrl) {
|
||||
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeGetErrorMessageOverride(error: BuildError, build: BuildFragment): string | null {
|
||||
if (!(error.errorCode in errorCodeToErrorMessageOverride)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return errorCodeToErrorMessageOverride[error.errorCode](build);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile, ResourceClass } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import semver from 'semver';
|
||||
|
||||
import { BuildResourceClass } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getReactNativeVersionAsync } from '../metadata';
|
||||
|
||||
type AndroidResourceClass = Exclude<
|
||||
ResourceClass,
|
||||
| ResourceClass.M1_EXPERIMENTAL
|
||||
| ResourceClass.M1_MEDIUM
|
||||
| ResourceClass.M1_LARGE
|
||||
| ResourceClass.INTEL_MEDIUM
|
||||
| ResourceClass.M_MEDIUM
|
||||
| ResourceClass.M_LARGE
|
||||
>;
|
||||
|
||||
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
|
||||
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
|
||||
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
|
||||
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
|
||||
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
|
||||
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
|
||||
};
|
||||
|
||||
const androidResourceClassToBuildResourceClassMapping: Record<
|
||||
AndroidResourceClass,
|
||||
BuildResourceClass
|
||||
> = {
|
||||
[ResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||
[ResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||
[ResourceClass.MEDIUM]: BuildResourceClass.AndroidMedium,
|
||||
};
|
||||
|
||||
export async function resolveBuildResourceClassAsync<T extends Platform>(
|
||||
profile: BuildProfile<T>,
|
||||
platform: Platform,
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
resourceClassFlag?: ResourceClass
|
||||
): Promise<BuildResourceClass> {
|
||||
const profileResourceClass = profile.resourceClass;
|
||||
|
||||
if (profileResourceClass && resourceClassFlag && resourceClassFlag !== profileResourceClass) {
|
||||
Log.warn(
|
||||
`Build profile specifies the "${profileResourceClass}" resource class but you passed "${resourceClassFlag}" to --resource-class.\nUsing the "${resourceClassFlag}" as the override.`
|
||||
);
|
||||
}
|
||||
|
||||
const selectedResourceClass = resourceClassFlag ?? profileResourceClass;
|
||||
|
||||
return platform === Platform.IOS
|
||||
? await resolveIosResourceClassAsync(exp, projectDir, resourceClassFlag ?? profileResourceClass)
|
||||
: resolveAndroidResourceClass(selectedResourceClass);
|
||||
}
|
||||
|
||||
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
|
||||
if (
|
||||
selectedResourceClass &&
|
||||
[
|
||||
ResourceClass.M1_EXPERIMENTAL,
|
||||
ResourceClass.M1_MEDIUM,
|
||||
ResourceClass.M1_LARGE,
|
||||
ResourceClass.INTEL_MEDIUM,
|
||||
].includes(selectedResourceClass)
|
||||
) {
|
||||
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
|
||||
}
|
||||
|
||||
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
|
||||
|
||||
return androidResourceClassToBuildResourceClassMapping[resourceClass as AndroidResourceClass];
|
||||
}
|
||||
|
||||
async function resolveIosResourceClassAsync(
|
||||
exp: ExpoConfig,
|
||||
projectDir: string,
|
||||
selectedResourceClass?: ResourceClass
|
||||
): Promise<BuildResourceClass> {
|
||||
const resourceClass =
|
||||
selectedResourceClass ?? (await resolveIosDefaultRequestedResourceClassAsync(exp, projectDir));
|
||||
|
||||
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
|
||||
Log.warn(
|
||||
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
|
||||
'm-medium'
|
||||
)} instead.`
|
||||
);
|
||||
}
|
||||
|
||||
if (resourceClass === ResourceClass.M1_LARGE) {
|
||||
Log.warn(
|
||||
`Resource class ${chalk.bold('m1-large')} is deprecated. Use ${chalk.bold(
|
||||
'm-large'
|
||||
)} instead.`
|
||||
);
|
||||
}
|
||||
|
||||
return iosResourceClassToBuildResourceClassMapping[resourceClass];
|
||||
}
|
||||
|
||||
async function resolveIosDefaultRequestedResourceClassAsync(
|
||||
exp: ExpoConfig,
|
||||
projectDir: string
|
||||
): Promise<ResourceClass> {
|
||||
const { sdkVersion } = exp;
|
||||
const reactNativeVersion = await getReactNativeVersionAsync(projectDir);
|
||||
if (
|
||||
(sdkVersion && semver.satisfies(sdkVersion, '>=48')) ||
|
||||
(reactNativeVersion && semver.satisfies(reactNativeVersion, '>=0.71.0'))
|
||||
) {
|
||||
return ResourceClass.M_MEDIUM;
|
||||
} else {
|
||||
return ResourceClass.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,14 @@ export function getProjectDashboardUrl(accountName: string, projectName: string)
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string {
|
||||
const { project } = build;
|
||||
const url =
|
||||
project.__typename !== 'App'
|
||||
? `/builds/${build.id}`
|
||||
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}`;
|
||||
: `/accounts/${project.ownerAccount.name}/projects/${project.slug}/builds/${build.id}${
|
||||
hash ? `#${hash}` : ''
|
||||
}`;
|
||||
|
||||
return new URL(url, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ export function getBranchMapping(branchMappingString?: string): {
|
||||
}
|
||||
|
||||
const isRollout = branchMapping.data.length === 2;
|
||||
const rolloutPercent = branchMapping.data[0].branchMappingLogic.operand;
|
||||
const rolloutPercent = branchMapping.data[0]?.branchMappingLogic.operand;
|
||||
|
||||
switch (branchMapping.data.length) {
|
||||
case 0:
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import ContextField, { ContextOptions } from './ContextField';
|
||||
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
|
||||
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
|
||||
|
||||
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
|
||||
export type DynamicConfigContextFn = (options: ExpoConfigOptions) => Promise<{
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
projectDir: string;
|
||||
@@ -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,
|
||||
|
||||
+95
-2
@@ -15,7 +15,6 @@ jest.mock('fs');
|
||||
|
||||
jest.mock('../../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../contextUtils/findProjectDirAndVerifyProjectSetupAsync');
|
||||
jest.mock('../../../../user/User');
|
||||
jest.mock('../../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
@@ -107,7 +106,79 @@ describe(getProjectIdAsync, () => {
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Project identified by "extra.eas.projectId" (notnotbrent) is not owned by owner specified in the "owner" field (wat). ${learnMore(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (notnotbrent) does not match owner specified in the "owner" field (wat). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the owner is not specified and is different than logged in user', async () => {
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManager,
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) does not match the logged in user (notnotbrent) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when the owner is not specified and is different than logged in user which is a robot', async () => {
|
||||
const sessionManagerMock = mock<SessionManager>();
|
||||
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
|
||||
actor: {
|
||||
__typename: 'Robot',
|
||||
id: 'robot_id',
|
||||
accounts: [
|
||||
{
|
||||
id: 'account_id_1',
|
||||
name: 'notnotbrent',
|
||||
users: [{ role: Role.Admin, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
{
|
||||
id: 'account_id_2',
|
||||
name: 'dominik',
|
||||
users: [{ role: Role.ViewOnly, actor: { id: 'robot_id' } }],
|
||||
},
|
||||
],
|
||||
isExpoAdmin: false,
|
||||
featureGates: {},
|
||||
},
|
||||
authenticationInfo: { accessToken: 'fake', sessionSecret: null },
|
||||
});
|
||||
const sessionManagerRobot = instance(sessionManagerMock);
|
||||
|
||||
jest.mocked(getConfig).mockReturnValue({
|
||||
exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
} as any);
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
fullName: '@totallybrent/test',
|
||||
slug: 'test',
|
||||
ownerAccount: { name: 'totallybrent' } as any,
|
||||
});
|
||||
|
||||
await expect(
|
||||
getProjectIdAsync(
|
||||
sessionManagerRobot,
|
||||
{ name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
|
||||
{ nonInteractive: false }
|
||||
)
|
||||
).rejects.toThrow(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (totallybrent) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "totallybrent" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
@@ -181,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(
|
||||
|
||||
@@ -10,7 +10,7 @@ import { getExpoConfig } from '../../../project/expoConfig';
|
||||
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { toAppPrivacy } from '../../../project/projectUtils';
|
||||
import SessionManager from '../../../user/SessionManager';
|
||||
import { Actor } from '../../../user/User';
|
||||
import { Actor, getActorUsername } from '../../../user/User';
|
||||
import { createGraphqlClient } from './createGraphqlClient';
|
||||
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
|
||||
|
||||
@@ -89,18 +89,50 @@ 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) {
|
||||
throw new Error(
|
||||
`Project config: Project identified by "extra.eas.projectId" (${
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) is not owned by owner specified in the "owner" field (${exp.owner}). ${learnMore(
|
||||
}) does not match owner specified in the "owner" field (${exp.owner}). ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
const actorUsername = getActorUsername(actor);
|
||||
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
|
||||
if (actorUsername) {
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) does not match the logged in user (${actorUsername}) and the "owner" field is not specified. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
// robot caller
|
||||
throw new Error(
|
||||
`Project config: Owner of project identified by "extra.eas.projectId" (${
|
||||
appForProjectId.ownerAccount.name
|
||||
}) must be specified in "owner" field when using a robot access token. To ensure all libraries work correctly, "owner": "${
|
||||
appForProjectId.ownerAccount.name
|
||||
}" should be added to the project config, which can be done automatically by re-running "eas init". ${learnMore(
|
||||
'https://expo.fyi/eas-project-id'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (exp.slug && exp.slug !== appForProjectId.slug) {
|
||||
throw new Error(
|
||||
`Project config: Slug for project identified by "extra.eas.projectId" (${
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export class EasCommandError extends Error {
|
||||
// constructor is not useless, since the constructor for Error allows for optional `message`
|
||||
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -227,6 +227,13 @@ export default class Build extends EasCommand {
|
||||
});
|
||||
} else if (process.platform !== 'darwin' && requestedPlatform === RequestedPlatform.Ios) {
|
||||
Errors.error('Unsupported platform, macOS is required to build apps for iOS', { exit: 1 });
|
||||
} else if (
|
||||
!['linux', 'darwin'].includes(process.platform) &&
|
||||
requestedPlatform === RequestedPlatform.Android
|
||||
) {
|
||||
Errors.error('Unsupported platform, macOS or Linux is required to build apps for Android', {
|
||||
exit: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,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)) ?? {};
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSetView);
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
loggedIn: { graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
} = await this.getContextAsync(BuildVersionSetView, {
|
||||
@@ -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,
|
||||
@@ -67,13 +67,14 @@ export default class BuildVersionSetView extends EasCommand {
|
||||
|
||||
validateAppConfigForRemoteVersionSource(exp, platform);
|
||||
|
||||
const applicationIdentifier = await getApplicationIdentifierAsync(
|
||||
const applicationIdentifier = await getApplicationIdentifierAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
profile,
|
||||
buildProfile: profile,
|
||||
platform,
|
||||
actor
|
||||
);
|
||||
});
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
|
||||
@@ -63,7 +63,7 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
public async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildVersionSyncView);
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
loggedIn: { graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
} = await this.getContextAsync(BuildVersionSyncView, {
|
||||
@@ -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);
|
||||
@@ -89,13 +89,14 @@ export default class BuildVersionSyncView extends EasCommand {
|
||||
validateAppConfigForRemoteVersionSource(exp, profileInfo.platform);
|
||||
const platformDisplayName = appPlatformDisplayNames[toAppPlatform(profileInfo.platform)];
|
||||
|
||||
const applicationIdentifier = await getApplicationIdentifierAsync(
|
||||
const applicationIdentifier = await getApplicationIdentifierAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
profileInfo.profile,
|
||||
profileInfo.platform,
|
||||
actor
|
||||
);
|
||||
buildProfile: profileInfo.profile,
|
||||
platform: profileInfo.platform,
|
||||
});
|
||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -344,12 +344,38 @@ describe(ProjectInit.name, () => {
|
||||
describe('when it is already configured', () => {
|
||||
beforeEach(() => {
|
||||
mockTestProject({ configuredProjectId: '1234', configuredOwner: jester.accounts[0].name });
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
});
|
||||
|
||||
it('does not configure', async () => {
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('checks owner and slug consistency', async () => {
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-124',
|
||||
fullName: '@jester2/testing-124',
|
||||
ownerAccount: jester2.accounts[0],
|
||||
});
|
||||
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(modifyConfigAsync).mockResolvedValue({ type: 'success', config: {} as any });
|
||||
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
|
||||
expect(confirmAsync).toHaveBeenCalled();
|
||||
expect(modifyConfigAsync).toHaveBeenCalledTimes(2);
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { owner: 'jester2' });
|
||||
expect(modifyConfigAsync).toHaveBeenCalledWith('/test-project', { slug: 'testing-124' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is not yet configured', () => {
|
||||
@@ -365,6 +391,14 @@ describe(ProjectInit.name, () => {
|
||||
|
||||
it('prompts for confirmation to link', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '123456');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
@@ -384,6 +418,14 @@ describe(ProjectInit.name, () => {
|
||||
it('asks to create it', async () => {
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('0129');
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '0129',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
await new ProjectInit([], commandOptions).run();
|
||||
expect(saveProjectIdToAppConfigAsync).toHaveBeenCalledWith('/test-project', '0129');
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -194,7 +194,6 @@ export default class ProjectInit extends EasCommand {
|
||||
}
|
||||
|
||||
private static async initializeWithExplicitIDAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string,
|
||||
projectDir: string,
|
||||
{ force, nonInteractive }: InitializeMethodOptions
|
||||
@@ -203,17 +202,13 @@ export default class ProjectInit extends EasCommand {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
await this.ensureOwnerSlugConsistencyAsync(graphqlClient, projectId, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
|
||||
private static async initializeWithInteractiveSelectionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
projectDir: string
|
||||
): Promise<void> {
|
||||
): Promise<string> {
|
||||
const exp = getExpoConfig(projectDir);
|
||||
const existingProjectId = exp.extra?.eas?.projectId;
|
||||
|
||||
@@ -223,7 +218,7 @@ export default class ProjectInit extends EasCommand {
|
||||
existingProjectId
|
||||
)}). To re-configure, remove the "extra.eas.projectId" field from your app config.`
|
||||
);
|
||||
return;
|
||||
return existingProjectId;
|
||||
}
|
||||
|
||||
const allAccounts = actor.accounts;
|
||||
@@ -287,7 +282,7 @@ export default class ProjectInit extends EasCommand {
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
|
||||
return;
|
||||
return existingProjectIdOnServer;
|
||||
}
|
||||
|
||||
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
|
||||
@@ -323,24 +318,36 @@ export default class ProjectInit extends EasCommand {
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, createdProjectId);
|
||||
return createdProjectId;
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const {
|
||||
flags: { id, force, 'non-interactive': nonInteractive },
|
||||
flags: { id: idArgument, force, 'non-interactive': nonInteractive },
|
||||
} = await this.parse(ProjectInit);
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
projectDir,
|
||||
} = await this.getContextAsync(ProjectInit, { nonInteractive });
|
||||
|
||||
if (id) {
|
||||
await ProjectInit.initializeWithExplicitIDAsync(graphqlClient, id, projectDir, {
|
||||
let idForConsistency: string;
|
||||
if (idArgument) {
|
||||
await ProjectInit.initializeWithExplicitIDAsync(idArgument, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
idForConsistency = idArgument;
|
||||
} else {
|
||||
await ProjectInit.initializeWithInteractiveSelectionAsync(graphqlClient, actor, projectDir);
|
||||
idForConsistency = await ProjectInit.initializeWithInteractiveSelectionAsync(
|
||||
graphqlClient,
|
||||
actor,
|
||||
projectDir
|
||||
);
|
||||
}
|
||||
|
||||
await ProjectInit.ensureOwnerSlugConsistencyAsync(graphqlClient, idForConsistency, projectDir, {
|
||||
force,
|
||||
nonInteractive,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
@@ -46,7 +55,7 @@ jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/publish', () => ({
|
||||
@@ -58,7 +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 */
|
||||
@@ -246,6 +320,8 @@ function mockTestExport({
|
||||
uniqueAssetCount: platforms.length,
|
||||
uniqueUploadedAssetCount: platforms.length,
|
||||
assetLimitPerUpdateGroup: 9001,
|
||||
launchAssetCount: 2,
|
||||
uniqueUploadedAssetPaths: [],
|
||||
});
|
||||
|
||||
jest.mocked(Updates.getRuntimeVersion).mockReturnValue(runtimeVersion);
|
||||
|
||||
@@ -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,6 +229,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
let unsortedUpdateInfoGroups: UpdateInfoGroup = {};
|
||||
let uploadedAssetCount = 0;
|
||||
let assetLimitPerUpdateGroup = 0;
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
|
||||
try {
|
||||
const collectedAssets = await collectAssetsAsync(distRoot);
|
||||
@@ -318,31 +248,42 @@ export default class UpdatePublish extends EasCommand {
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
|
||||
: `Uploaded: No changes detected`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
|
||||
// NOTE(cedric): we assume that bundles are always uploaded, and always are part of
|
||||
// `uploadedAssetCount`, perferably we don't assume. For that, we need to refactor the
|
||||
// `uploadAssetsAsync` and be able to determine asset type from the uploaded assets.
|
||||
const uploadedBundleCount = uploadResults.launchAssetCount;
|
||||
const uploadedNormalAssetCount = Math.max(0, uploadedAssetCount - uploadedBundleCount);
|
||||
const reusedNormalAssetCount = uploadResults.uniqueAssetCount - uploadedNormalAssetCount;
|
||||
|
||||
assetSpinner.stop();
|
||||
Log.withTick(
|
||||
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
|
||||
);
|
||||
if (uploadedNormalAssetCount === 0) {
|
||||
Log.withTick(`Uploading assets skipped - no new assets found`);
|
||||
} else {
|
||||
let message = `Uploaded ${uploadedNormalAssetCount} ${
|
||||
uploadedNormalAssetCount === 1 ? 'asset' : 'assets'
|
||||
}`;
|
||||
if (reusedNormalAssetCount > 0) {
|
||||
message += ` (reused ${reusedNormalAssetCount} ${
|
||||
reusedNormalAssetCount === 1 ? 'asset' : 'assets'
|
||||
})`;
|
||||
}
|
||||
Log.withTick(message);
|
||||
}
|
||||
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
|
||||
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
|
||||
}
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload');
|
||||
throw e;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -377,7 +318,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
branchId,
|
||||
updateInfoGroup: localUpdateInfoGroup,
|
||||
runtimeVersion,
|
||||
message: truncatedMessage,
|
||||
message: updateMessage,
|
||||
gitCommitHash,
|
||||
isGitWorkingTreeDirty,
|
||||
awaitingCodeSigningInfo: !!codeSigningInfo,
|
||||
@@ -394,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,
|
||||
@@ -414,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,
|
||||
@@ -480,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
|
||||
? [
|
||||
{
|
||||
@@ -551,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import { getAppFromContextAsync } from './actions/BuildCredentialsUtils';
|
||||
import { SetUpBuildCredentials } from './actions/SetUpBuildCredentials';
|
||||
import { SetUpPushKey } from './actions/SetUpPushKey';
|
||||
import { App, IosCredentials, Target } from './types';
|
||||
import { isAdHocProfile } from './utils/provisioningProfile';
|
||||
import { isAdHocProfile, isEnterpriseUniversalProfile } from './utils/provisioningProfile';
|
||||
|
||||
interface Options {
|
||||
app: App;
|
||||
@@ -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 = {
|
||||
@@ -143,17 +143,32 @@ export default class IosCredentialsProvider {
|
||||
|
||||
private assertProvisioningProfileType(provisioningProfile: string, targetName?: string): void {
|
||||
const isAdHoc = isAdHocProfile(provisioningProfile);
|
||||
if (this.options.distribution === 'internal' && !isAdHoc) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution`
|
||||
);
|
||||
} else if (this.options.distribution !== 'internal' && isAdHoc) {
|
||||
const isEnterprise = isEnterpriseUniversalProfile(provisioningProfile);
|
||||
if (this.options.distribution === 'internal') {
|
||||
if (this.options.enterpriseProvisioning === 'universal' && !isEnterprise) {
|
||||
throw new Error(
|
||||
`You must use a universal provisioning profile${
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution if you specified "enterpriseProvisioning": "universal" in eas.json`
|
||||
);
|
||||
} else if (this.options.enterpriseProvisioning === 'adhoc' && !isAdHoc) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution if you specified "enterpriseProvisioning": "adhoc" in eas.json`
|
||||
);
|
||||
} else if (!this.options.enterpriseProvisioning && !isEnterprise && !isAdHoc) {
|
||||
throw new Error(
|
||||
`You must use an adhoc provisioning profile${
|
||||
targetName ? ` (target '${targetName})'` : ''
|
||||
} for internal distribution.`
|
||||
);
|
||||
}
|
||||
} else if (isAdHoc) {
|
||||
throw new Error(
|
||||
`You can't use an adhoc provisioning profile${
|
||||
targetName ? ` (target '${targetName}')` : ''
|
||||
} for app store distribution`
|
||||
} for app store distribution.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -348,7 +348,7 @@ describe(syncCapabilitiesForEntitlementsAsync, () => {
|
||||
'com.apple.developer.healthkit': true,
|
||||
})
|
||||
).rejects.toThrowError(
|
||||
`https://developer.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
|
||||
`https://developer-mdn.apple.com/account/resources/identifiers/bundleId/edit/XXX333`
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { formatContractMessage } from '../contractMessages';
|
||||
const expiredMembership = {
|
||||
subject: 'Developer Program Membership Expired',
|
||||
message:
|
||||
"Your membership has expired, and your apps have been removed from the App Store until you renew your membership. To renew, a user with the Account Holder role must <a href='http://developer.apple.com/membercenter/index.action' target='_blank'>sign in</a> and renew the membership on the Apple Developer website. <a href='https://developer.apple.com/support/renewal/' target='_blank'>Learn More</a>.",
|
||||
"Your membership has expired, and your apps have been removed from the App Store until you renew your membership. To renew, a user with the Account Holder role must <a href='http://developer-mdn.apple.com/membercenter/index.action' target='_blank'>sign in</a> and renew the membership on the Apple Developer website. <a href='https://developer-mdn.apple.com/support/renewal/' target='_blank'>Learn More</a>.",
|
||||
};
|
||||
const paymentReturned = {
|
||||
subject: 'Payment Returned',
|
||||
@@ -15,12 +15,12 @@ const paymentReturned = {
|
||||
const licenseAgreementUpdated = {
|
||||
subject: 'Apple Developer Program License Agreement Updated',
|
||||
message:
|
||||
'The updated Apple Developer Program License Agreement needs to be reviewed. In order to update your existing apps and submit new apps to the App Store, the Account Holder must review and accept the updated agreement by signing in to their <a href="https://developer.apple.com/account" target="_blank">account</a> on the Apple Developer website.',
|
||||
'The updated Apple Developer Program License Agreement needs to be reviewed. In order to update your existing apps and submit new apps to the App Store, the Account Holder must review and accept the updated agreement by signing in to their <a href="https://developer-mdn.apple.com/account" target="_blank">account</a> on the Apple Developer website.',
|
||||
};
|
||||
const paidLicenseAgreementUpdated = {
|
||||
subject: '',
|
||||
message:
|
||||
'<b>Review the updated Paid Applications Schedule.</b><br />In order to update your existing apps, create new in-app purchases, and submit new apps to the App Store, the user with the Legal role (Account Holder) must review and accept the Paid Applications Schedule (Schedule 2 to the Apple Developer Program License Agreement) in the Agreements, Tax, and Banking module.<br /><br /> To accept this agreement, they must have already accepted the latest version of the Apple Developer Program License Agreement in their <a href="http://developer.apple.com/membercenter/index.action">account on the developer website<a/>.<br />',
|
||||
'<b>Review the updated Paid Applications Schedule.</b><br />In order to update your existing apps, create new in-app purchases, and submit new apps to the App Store, the user with the Legal role (Account Holder) must review and accept the Paid Applications Schedule (Schedule 2 to the Apple Developer Program License Agreement) in the Agreements, Tax, and Banking module.<br /><br /> To accept this agreement, they must have already accepted the latest version of the Apple Developer Program License Agreement in their <a href="http://developer-mdn.apple.com/membercenter/index.action">account on the developer website<a/>.<br />',
|
||||
};
|
||||
|
||||
describe(formatContractMessage, () => {
|
||||
|
||||
@@ -111,7 +111,7 @@ export async function syncCapabilitiesForEntitlementsAsync(
|
||||
inspect(modifiedRequest, { depth: null, colors: true })
|
||||
);
|
||||
throw new Error(
|
||||
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
|
||||
`Unexpected error occurred while attempting to update capabilities for app "${bundleId.attributes.identifier}".\nCapabilities can be modified manually in the Apple developer console at https://developer-mdn.apple.com/account/resources/identifiers/bundleId/edit/${bundleId.id}.\nAuto capability syncing can be disabled with the environment variable \`EXPO_NO_CAPABILITY_SYNC=1\`.\n${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,7 @@ type CapabilityClassifier = {
|
||||
|
||||
// NOTE(Bacon): From manually toggling values in Xcode and checking the git diff and network requests.
|
||||
// Last Updated: July 22nd, 2021
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements
|
||||
export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
{
|
||||
name: 'HomeKit',
|
||||
@@ -413,7 +413,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_default-data-protection
|
||||
name: 'Data Protection',
|
||||
entitlement: 'com.apple.developer.default-data-protection',
|
||||
capability: CapabilityType.DATA_PROTECTION,
|
||||
@@ -446,7 +446,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
getOptions: getBooleanOptions,
|
||||
},
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_networkextension
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_networkextension
|
||||
name: 'Network Extensions',
|
||||
entitlement: 'com.apple.developer.networking.networkextension',
|
||||
capability: CapabilityType.NETWORK_EXTENSIONS,
|
||||
@@ -465,7 +465,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
getOptions: getDefinedOptions,
|
||||
},
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_nfc_readersession_formats
|
||||
name: 'NFC Tag Reading',
|
||||
entitlement: 'com.apple.developer.nfc.readersession.formats',
|
||||
capability: CapabilityType.NFC_TAG_READING,
|
||||
@@ -482,7 +482,7 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
getOptions: getDefinedOptions,
|
||||
},
|
||||
{
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_vpn_api
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_vpn_api
|
||||
name: 'Push Notifications',
|
||||
// com.apple.developer.aps-environment
|
||||
entitlement: 'aps-environment',
|
||||
@@ -515,8 +515,8 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
},
|
||||
{
|
||||
// Not in Xcode
|
||||
// https://developer.apple.com/documentation/devicecheck/preparing_to_use_the_app_attest_service
|
||||
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_devicecheck_appattest-environment
|
||||
// https://developer-mdn.apple.com/documentation/devicecheck/preparing_to_use_the_app_attest_service
|
||||
// https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_devicecheck_appattest-environment
|
||||
name: 'App Attest',
|
||||
entitlement: 'com.apple.developer.devicecheck.appattest-environment',
|
||||
capability: CapabilityType.APP_ATTEST,
|
||||
@@ -725,10 +725,10 @@ export const CapabilityMapping: CapabilityClassifier[] = [
|
||||
// it's not clear if it needs to be updated.
|
||||
|
||||
// "Contact Notes" requires the user to ask Apple in a form:
|
||||
// https://developer.apple.com/contact/request/contact-note-field
|
||||
// com.apple.developer.contacts.notes: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes/
|
||||
// https://developer-mdn.apple.com/contact/request/contact-note-field
|
||||
// com.apple.developer.contacts.notes: https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_contacts_notes/
|
||||
|
||||
// "Exposure Notification" requires the user to ask Apple in a form:
|
||||
// https://developer.apple.com/contact/request/exposure-notification-entitlement
|
||||
// com.apple.developer.exposure-notification: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_exposure-notification/
|
||||
// https://developer-mdn.apple.com/contact/request/exposure-notification-entitlement
|
||||
// com.apple.developer.exposure-notification: https://developer-mdn.apple.com/documentation/bundleresources/entitlements/com_apple_developer_exposure-notification/
|
||||
];
|
||||
@@ -26,6 +26,11 @@ export function isAdHocProfile(dataBase64: string): boolean {
|
||||
return Array.isArray(provisionedDevices);
|
||||
}
|
||||
|
||||
export function isEnterpriseUniversalProfile(dataBase64: string): boolean {
|
||||
const profilePlist = parse(dataBase64);
|
||||
return !!profilePlist['ProvisionsAllDevices'];
|
||||
}
|
||||
|
||||
export function parse(dataBase64: string): PlistObject {
|
||||
try {
|
||||
const buffer = Buffer.from(dataBase64, 'base64');
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
+628
-129
File diff suppressed because it is too large.
Load diff
@@ -18,6 +18,8 @@ import {
|
||||
IosJobOverridesInput,
|
||||
RetryIosBuildMutation,
|
||||
RetryIosBuildMutationVariables,
|
||||
UpdateBuildMetadataMutation,
|
||||
UpdateBuildMetadataMutationVariables,
|
||||
} from '../generated';
|
||||
import { BuildFragmentNode } from '../types/Build';
|
||||
|
||||
@@ -119,6 +121,33 @@ export const BuildMutation = {
|
||||
);
|
||||
return nullthrows(data.build?.createIosBuild);
|
||||
},
|
||||
async updateBuildMetadataAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: {
|
||||
buildId: string;
|
||||
metadata: BuildMetadataInput;
|
||||
}
|
||||
): Promise<BuildFragment> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdateBuildMetadataMutation, UpdateBuildMetadataMutationVariables>(
|
||||
gql`
|
||||
mutation UpdateBuildMetadataMutation($buildId: ID!, $metadata: BuildMetadataInput!) {
|
||||
build {
|
||||
updateBuildMetadata(buildId: $buildId, metadata: $metadata) {
|
||||
id
|
||||
...BuildFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(BuildFragmentNode)}
|
||||
`,
|
||||
input
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return nullthrows(data.build?.updateBuildMetadata);
|
||||
},
|
||||
async retryIosBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
input: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -52,6 +52,7 @@ export const BuildFragmentNode = gql`
|
||||
priority
|
||||
createdAt
|
||||
updatedAt
|
||||
message
|
||||
completedAt
|
||||
resourceClass
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
isRollBackToEmbedded
|
||||
manifestPermalink
|
||||
gitCommitHash
|
||||
actor {
|
||||
|
||||
@@ -90,11 +90,12 @@ export default class Log {
|
||||
*/
|
||||
export function link(
|
||||
url: string,
|
||||
{ text = url, dim = true }: { text?: string; dim?: boolean } = {}
|
||||
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
|
||||
): string {
|
||||
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
||||
const output = terminalLink(text, url, {
|
||||
fallback: () => (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
|
||||
fallback: () =>
|
||||
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
|
||||
});
|
||||
return dim ? chalk.dim(output) : output;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const KEYWORD_CHARACTER_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* Keywords are limited to 100 characters when converted to a comma separated string.
|
||||
* @see https://developer.apple.com/app-store/search/
|
||||
* @see https://developer-mdn.apple.com/app-store/search/
|
||||
*/
|
||||
export const infoKeywordLength: IssueRule = {
|
||||
id: 'apple.info.keyword.length',
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface AppleRelease {
|
||||
export interface AppleInfo {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
/** Does not effect ASO https://developer.apple.com/app-store/product-page/ */
|
||||
/** Does not effect ASO https://developer-mdn.apple.com/app-store/product-page/ */
|
||||
promoText?: string;
|
||||
description?: string;
|
||||
keywords?: string[];
|
||||
|
||||
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -14,9 +13,12 @@ import {
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
defaultPublishPlatforms,
|
||||
filterExportedPlatformsByFlag,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getAssetHashFromPath,
|
||||
getBase64URLEncoding,
|
||||
getOriginalPathFromAssetMap,
|
||||
getStorageKey,
|
||||
getStorageKeyForAssetAsync,
|
||||
guessContentTypeFromExtension,
|
||||
@@ -279,9 +281,55 @@ describe(resolveInputDirectoryAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(getAssetHashFromPath, () => {
|
||||
it('returns asset hash from path', () => {
|
||||
expect(getAssetHashFromPath('assets/5b2a819c71d035ca45d223e4c47ed4f9')).toBe(
|
||||
'5b2a819c71d035ca45d223e4c47ed4f9'
|
||||
);
|
||||
});
|
||||
it('returns null for incorrect path', () => {
|
||||
expect(getAssetHashFromPath('assets/this/is/not/a/hash.jpg')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(getOriginalPathFromAssetMap, () => {
|
||||
// Partial assetmap.json, with only fields we need
|
||||
const fakeAssetMap = {
|
||||
'5b2a819c71d035ca45d223e4c47ed4f9': {
|
||||
httpServerLocation: '/assets/src/assets',
|
||||
name: 'asset-420',
|
||||
type: 'jpg',
|
||||
},
|
||||
};
|
||||
it('returns null path when asset map is null', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(null, {
|
||||
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
it('returns null when asset is not found in asset map', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(fakeAssetMap, {
|
||||
path: 'assets/fb64d3b2fb71b3d739ad5c13a93e12c5',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
it('returns reconstructed original path from existing asset in asset map', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(fakeAssetMap, {
|
||||
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBe('/src/assets/asset-420.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectAssetsAsync, () => {
|
||||
it('builds an update info group', async () => {
|
||||
const fakeHash = 'md5-hash-of-jpg';
|
||||
const fakeHash = 'hdahukw8adhawi8fawhfa8';
|
||||
const bundles = {
|
||||
android: 'android-bundle-code',
|
||||
ios: 'ios-bundle-code',
|
||||
@@ -294,6 +342,7 @@ describe(collectAssetsAsync, () => {
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: `${inputDir}/assets/${fakeHash}`,
|
||||
originalPath: `assets/wat.jpg`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -326,6 +375,24 @@ describe(collectAssetsAsync, () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.resolve(inputDir, 'assetmap.json'),
|
||||
JSON.stringify({
|
||||
[fakeHash]: {
|
||||
__packager_asset: true,
|
||||
fileSystemLocation: '/Users/blah/temp/assets',
|
||||
httpServerLocation: 'assets/assets',
|
||||
width: 2339,
|
||||
height: 1560,
|
||||
scales: [1],
|
||||
files: ['/Users/blah/temp/assets/wat.jpg'],
|
||||
hash: fakeHash,
|
||||
name: 'wat',
|
||||
type: 'jpg',
|
||||
fileHashes: [fakeHash],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(await collectAssetsAsync(inputDir)).toEqual({
|
||||
android: {
|
||||
@@ -410,6 +477,7 @@ describe(uploadAssetsAsync, () => {
|
||||
const androidBundlePath = uuidv4();
|
||||
const iosBundlePath = uuidv4();
|
||||
const dummyFilePath = uuidv4();
|
||||
const dummyOriginalFilePath = uuidv4();
|
||||
const userDefinedPath = uuidv4();
|
||||
const testProjectId = uuidv4();
|
||||
const expectedAssetLimit = 1400;
|
||||
@@ -427,7 +495,15 @@ describe(uploadAssetsAsync, () => {
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
@@ -435,7 +511,15 @@ describe(uploadAssetsAsync, () => {
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -490,8 +574,10 @@ describe(uploadAssetsAsync, () => {
|
||||
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
|
||||
).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
launchAssetCount: 2,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 0,
|
||||
uniqueUploadedAssetPaths: [],
|
||||
assetLimitPerUpdateGroup: expectedAssetLimit,
|
||||
});
|
||||
});
|
||||
@@ -526,8 +612,10 @@ describe(uploadAssetsAsync, () => {
|
||||
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
|
||||
).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
launchAssetCount: 2,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 2,
|
||||
uniqueUploadedAssetPaths: [],
|
||||
assetLimitPerUpdateGroup: expectedAssetLimit,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester, jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
@@ -11,6 +14,7 @@ import {
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -103,6 +107,8 @@ describe(getApplicationIdAsync, () => {
|
||||
describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
describe('managed project + android.package missing in app config', () => {
|
||||
it('throws an error if using app.config.js', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.config.js': 'module.exports = { blah: {} };',
|
||||
@@ -110,10 +116,23 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
'/app'
|
||||
);
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the Android package if using app.json', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
@@ -126,11 +145,26 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
}));
|
||||
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '',
|
||||
exp: {} as any,
|
||||
})
|
||||
).resolves.toBe('com.expo.notdominik');
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('puts the Android package in app.json', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
@@ -143,7 +177,12 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => {
|
||||
}));
|
||||
|
||||
await expect(
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '',
|
||||
exp: {} as any,
|
||||
})
|
||||
).resolves.toBe('com.expo.notdominik');
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
|
||||
@@ -7,22 +7,31 @@ import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { readAppJson } from '../../build/utils/appJson';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import env from '../../env';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { getProjectConfigDescription, getUsername } from '../../project/projectUtils';
|
||||
import {
|
||||
getOwnerAccountForProjectIdAsync,
|
||||
getProjectConfigDescription,
|
||||
} from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
import { GradleBuildContext } from './gradle';
|
||||
import * as gradleUtils from './gradleUtils';
|
||||
|
||||
export const INVALID_APPLICATION_ID_MESSAGE = `Invalid format of Android applicationId. Only alphanumeric characters, '.' and '_' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
): Promise<string> {
|
||||
export async function ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
|
||||
|
||||
@@ -31,7 +40,7 @@ export async function ensureApplicationIdIsDefinedForManagedProjectAsync(
|
||||
moduleName: gradleUtils.DEFAULT_MODULE_NAME,
|
||||
});
|
||||
} catch {
|
||||
return await configureApplicationIdAsync(projectDir, exp, actor);
|
||||
return await configureApplicationIdAsync({ graphqlClient, projectDir, projectId, exp });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,11 +119,17 @@ export async function getApplicationIdAsync(
|
||||
}
|
||||
}
|
||||
|
||||
async function configureApplicationIdAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
): Promise<string> {
|
||||
async function configureApplicationIdAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
@@ -132,7 +147,11 @@ async function configureApplicationIdAsync(
|
||||
)}`
|
||||
);
|
||||
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(exp, actor);
|
||||
const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
const { packageName } = await promptAsync({
|
||||
name: 'packageName',
|
||||
type: 'text',
|
||||
@@ -174,8 +193,9 @@ export function warnIfAndroidPackageDefinedInAppConfigForBareWorkflowProject(
|
||||
}
|
||||
|
||||
async function getSuggestedApplicationIdAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
projectId: string
|
||||
): Promise<string | undefined> {
|
||||
// Attempt to use the ios bundle id first since it's convenient to have them aligned.
|
||||
const maybeBundleId = IOSConfig.BundleIdentifier.getBundleIdentifier(exp);
|
||||
@@ -183,9 +203,9 @@ async function getSuggestedApplicationIdAsync(
|
||||
return maybeBundleId;
|
||||
} else {
|
||||
// the only callsite is heavily interactive
|
||||
const username = getUsername(exp, actor);
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
// It's common to use dashes in your node project name, strip them from the suggested package name.
|
||||
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
|
||||
const possibleId = `com.${account.name}.${exp.slug}`.split('-').join('');
|
||||
if (isApplicationIdValid(possibleId)) {
|
||||
return possibleId;
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ExpoConfig } from '@expo/config';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { Actor } from '../user/User';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
@@ -16,19 +16,32 @@ import { resolveXcodeBuildContextAsync } from './ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from './ios/target';
|
||||
import { resolveWorkflowAsync } from './workflow';
|
||||
|
||||
export async function getApplicationIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile,
|
||||
platform: Platform,
|
||||
actor: Actor
|
||||
): Promise<string> {
|
||||
export async function getApplicationIdentifierAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
buildProfile,
|
||||
platform,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
buildProfile: BuildProfile;
|
||||
platform: Platform;
|
||||
}): Promise<string> {
|
||||
if (platform === Platform.ANDROID) {
|
||||
const profile = buildProfile as BuildProfile<Platform.ANDROID>;
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||
|
||||
if (workflow === Workflow.MANAGED) {
|
||||
return await ensureApplicationIdIsDefinedForManagedProjectAsync(projectDir, exp, actor);
|
||||
return await ensureApplicationIdIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
});
|
||||
}
|
||||
|
||||
const gradleContext = await resolveGradleBuildContextAsync(projectDir, profile);
|
||||
@@ -37,7 +50,12 @@ export async function getApplicationIdentifierAsync(
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
const profile = buildProfile as BuildProfile<Platform.IOS>;
|
||||
if (workflow === Workflow.MANAGED) {
|
||||
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync(projectDir, exp, actor);
|
||||
return await ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
});
|
||||
}
|
||||
|
||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -2,8 +2,11 @@ import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester, jester as mockJester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync,
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../user/actions', () => ({ ensureLoggedInAsync: jest.fn(() => mockJester) }));
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -86,6 +90,7 @@ describe(getBundleIdentifierAsync, () => {
|
||||
describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
describe('managed project + ios.bundleIdentifier missing in app config', () => {
|
||||
it('throws an error if using app.config.js', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.config.js': 'module.exports = { blah: {} };',
|
||||
@@ -93,10 +98,23 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
'/app'
|
||||
);
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: {} as any,
|
||||
})
|
||||
).rejects.toThrowError(/we can't update this file programmatically/);
|
||||
});
|
||||
it('prompts for the bundle identifier if using app.json', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
@@ -109,11 +127,24 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
}));
|
||||
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: {} as any,
|
||||
})
|
||||
).resolves.toBe('com.expo.notdominik');
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
});
|
||||
it('puts the bundle identifier in app.json', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
|
||||
id: '1234',
|
||||
slug: 'testing-123',
|
||||
fullName: '@jester/testing-123',
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
vol.fromJSON(
|
||||
{
|
||||
'app.json': '{ "expo": {} }',
|
||||
@@ -126,7 +157,12 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => {
|
||||
}));
|
||||
|
||||
await expect(
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync('/app', {} as any, mockJester)
|
||||
ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir: '/app',
|
||||
projectId: '1234',
|
||||
exp: {} as any,
|
||||
})
|
||||
).resolves.toBe('com.expo.notdominik');
|
||||
const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8'));
|
||||
expect(appJson).toMatchObject({
|
||||
|
||||
@@ -6,26 +6,37 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { readAppJson } from '../../build/utils/appJson';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { Actor } from '../../user/User';
|
||||
import { getProjectConfigDescription, getUsername } from '../projectUtils';
|
||||
import { getOwnerAccountForProjectIdAsync, getProjectConfigDescription } from '../projectUtils';
|
||||
import { resolveWorkflowAsync } from '../workflow';
|
||||
|
||||
export const INVALID_BUNDLE_IDENTIFIER_MESSAGE = `Invalid format of iOS bundle identifier. Only alphanumeric characters, '.' and '-' are allowed, and each '.' must be followed by a letter.`;
|
||||
|
||||
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
): Promise<string> {
|
||||
export async function ensureBundleIdentifierIsDefinedForManagedProjectAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||
assert(workflow === Workflow.MANAGED, 'This function should be called only for managed projects');
|
||||
|
||||
try {
|
||||
return await getBundleIdentifierAsync(projectDir, exp);
|
||||
} catch {
|
||||
return await configureBundleIdentifierAsync(projectDir, exp, actor);
|
||||
return await configureBundleIdentifierAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
exp,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,11 +111,17 @@ export async function getBundleIdentifierAsync(
|
||||
}
|
||||
}
|
||||
|
||||
async function configureBundleIdentifierAsync(
|
||||
projectDir: string,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
): Promise<string> {
|
||||
async function configureBundleIdentifierAsync({
|
||||
graphqlClient,
|
||||
projectDir,
|
||||
projectId,
|
||||
exp,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectDir: string;
|
||||
projectId: string;
|
||||
exp: ExpoConfig;
|
||||
}): Promise<string> {
|
||||
const paths = getConfigFilePaths(projectDir);
|
||||
// we can't automatically update app.config.js
|
||||
if (paths.dynamicConfigPath) {
|
||||
@@ -122,7 +139,11 @@ async function configureBundleIdentifierAsync(
|
||||
)}`
|
||||
);
|
||||
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(exp, actor);
|
||||
const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient,
|
||||
exp,
|
||||
projectId
|
||||
);
|
||||
const { bundleIdentifier } = await promptAsync({
|
||||
name: 'bundleIdentifier',
|
||||
type: 'text',
|
||||
@@ -169,8 +190,9 @@ export function isWildcardBundleIdentifier(bundleIdentifier: string): boolean {
|
||||
}
|
||||
|
||||
async function getSuggestedBundleIdentifierAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
exp: ExpoConfig,
|
||||
actor: Actor
|
||||
projectId: string
|
||||
): Promise<string | undefined> {
|
||||
// Attempt to use the android package name first since it's convenient to have them aligned.
|
||||
const maybeAndroidPackage = AndroidConfig.Package.getPackage(exp);
|
||||
@@ -178,9 +200,9 @@ async function getSuggestedBundleIdentifierAsync(
|
||||
return maybeAndroidPackage;
|
||||
} else {
|
||||
// the only callsite is heavily interactive
|
||||
const username = getUsername(exp, actor);
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
// It's common to use dashes in your node project name, strip them from the suggested package name.
|
||||
const possibleId = `com.${username}.${exp.slug}`.split('-').join('');
|
||||
const possibleId = `com.${account.name}.${exp.slug}`.split('-').join('');
|
||||
if (isBundleIdentifierValid(possibleId)) {
|
||||
return possibleId;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ export function getApplePlatformFromSdkRoot(target: Target): ApplePlatform | nul
|
||||
* Get Apple Platform from the Xcode TARGETED_DEVICE_FAMILY where possible.
|
||||
*
|
||||
* References:
|
||||
* https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
|
||||
* https://developer-mdn.apple.com/library/archive/documentation/DeveloperTools/Reference/XcodeBuildSettingRef/1-Build_Setting_Reference/build_setting_ref.html
|
||||
* https://stackoverflow.com/questions/39677524/xcode-8-how-to-change-targeted-device-family#comment100316573_39677659
|
||||
*
|
||||
* @returns - Apple Platform when known, defaults to null when unknown
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { ExpoConfig, Platform } from '@expo/config';
|
||||
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 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 uniqBy from '../utils/expodash/uniqBy';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { resolveWorkflowAsync } from './workflow';
|
||||
|
||||
export type ExpoCLIExportPlatformFlag = Platform | 'all';
|
||||
|
||||
@@ -30,6 +45,8 @@ export type RawAsset = {
|
||||
fileExtension?: string;
|
||||
contentType: string;
|
||||
path: string;
|
||||
/** Original asset path derrived from asset map, or exported folder */
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
type CollectedAssets = {
|
||||
@@ -52,6 +69,16 @@ type UpdateInfoGroup = {
|
||||
[key in Platform]: ManifestFragment;
|
||||
};
|
||||
|
||||
// Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes`
|
||||
type AssetMap = Record<
|
||||
string,
|
||||
{
|
||||
httpServerLocation: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const fileMetadataJoi = Joi.object({
|
||||
assets: Joi.array()
|
||||
.required()
|
||||
@@ -170,6 +197,7 @@ export async function buildBundlesAsync({
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
'--dump-assetmap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
@@ -182,6 +210,7 @@ export async function buildBundlesAsync({
|
||||
'--experimental-bundle',
|
||||
'--non-interactive',
|
||||
'--dump-sourcemap',
|
||||
'--dump-assetmap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
@@ -246,9 +275,48 @@ export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform,
|
||||
return { [platform]: record[platform] } as T;
|
||||
}
|
||||
|
||||
/** Try to load the asset map for logging the names of assets published */
|
||||
export async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
|
||||
const assetMapPath = path.join(distRoot, 'assetmap.json');
|
||||
|
||||
if (!(await fs.pathExists(assetMapPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetMap: AssetMap = JsonFile.read(path.join(distRoot, 'assetmap.json'));
|
||||
// TODO: basic validation?
|
||||
return assetMap;
|
||||
}
|
||||
|
||||
// exposed for testing
|
||||
export function getAssetHashFromPath(assetPath: string): string | null {
|
||||
const [, hash] = assetPath.match(new RegExp(/assets\/([a-z0-9]+)$/, 'i')) ?? [];
|
||||
return hash ?? null;
|
||||
}
|
||||
|
||||
// exposed for testing
|
||||
export function getOriginalPathFromAssetMap(
|
||||
assetMap: AssetMap | null,
|
||||
asset: { path: string; ext: string }
|
||||
): string | null {
|
||||
if (!assetMap) {
|
||||
return null;
|
||||
}
|
||||
const assetHash = getAssetHashFromPath(asset.path);
|
||||
const assetMapEntry = assetHash && assetMap[assetHash];
|
||||
|
||||
if (!assetMapEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathPrefix = assetMapEntry.httpServerLocation.substring('/assets'.length);
|
||||
return `${pathPrefix}/${assetMapEntry.name}.${assetMapEntry.type}`;
|
||||
}
|
||||
|
||||
/** Given a directory, load the metadata.json and collect the assets for each platform. */
|
||||
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
|
||||
const metadata = loadMetadata(dir);
|
||||
const assetmap = await loadAssetMapAsync(dir);
|
||||
|
||||
const collectedAssets: CollectedAssets = {};
|
||||
|
||||
@@ -261,6 +329,7 @@ export async function collectAssetsAsync(dir: string): Promise<CollectedAssets>
|
||||
},
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => ({
|
||||
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
|
||||
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(dir, asset.path),
|
||||
})),
|
||||
@@ -294,9 +363,17 @@ export async function filterOutAssetsThatAlreadyExistAsync(
|
||||
}
|
||||
|
||||
type AssetUploadResult = {
|
||||
/** All found assets within the exported folder per platform */
|
||||
assetCount: number;
|
||||
/** The uploaded JS bundles, per platform */
|
||||
launchAssetCount: number;
|
||||
/** All unique assets within the exported folder with platforms combined */
|
||||
uniqueAssetCount: number;
|
||||
/** All unique assets uploaded */
|
||||
uniqueUploadedAssetCount: number;
|
||||
/** All (non-launch) asset original paths, used for logging */
|
||||
uniqueUploadedAssetPaths: string[];
|
||||
/** The asset limit received from the server */
|
||||
assetLimitPerUpdateGroup: number;
|
||||
};
|
||||
|
||||
@@ -308,7 +385,9 @@ export async function uploadAssetsAsync(
|
||||
): Promise<AssetUploadResult> {
|
||||
let assets: RawAsset[] = [];
|
||||
let platform: keyof CollectedAssets;
|
||||
const launchAssets: RawAsset[] = [];
|
||||
for (platform in assetsForUpdateInfoGroup) {
|
||||
launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset);
|
||||
assets = [
|
||||
...assets,
|
||||
assetsForUpdateInfoGroup[platform]!.launchAsset,
|
||||
@@ -335,6 +414,7 @@ export async function uploadAssetsAsync(
|
||||
updateSpinnerText?.(totalAssets, totalAssets);
|
||||
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(graphqlClient, uniqueAssets);
|
||||
const uniqueUploadedAssetCount = missingAssets.length;
|
||||
const uniqueUploadedAssetPaths = missingAssets.map(asset => asset.originalPath).filter(truthy);
|
||||
|
||||
const missingAssetChunks = chunk(missingAssets, 100);
|
||||
const specifications: string[] = [];
|
||||
@@ -372,8 +452,10 @@ export async function uploadAssetsAsync(
|
||||
}
|
||||
return {
|
||||
assetCount: assets.length,
|
||||
launchAssetCount: launchAssets.length,
|
||||
uniqueAssetCount: uniqueAssets.length,
|
||||
uniqueUploadedAssetCount,
|
||||
uniqueUploadedAssetPaths,
|
||||
assetLimitPerUpdateGroup,
|
||||
};
|
||||
}
|
||||
@@ -385,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;
|
||||
}
|
||||
@@ -317,30 +317,48 @@ async function handleBuildListSourceAsync(
|
||||
}
|
||||
|
||||
function formatBuildChoice(build: BuildFragment, expiryDate: Date): prompts.Choice {
|
||||
const { id, updatedAt, runtimeVersion, buildProfile, gitCommitHash, gitCommitMessage, channel } =
|
||||
build;
|
||||
|
||||
const formatValue = (field?: string | null): string => (field ? chalk.bold(field) : 'Unknown');
|
||||
|
||||
const {
|
||||
id,
|
||||
updatedAt,
|
||||
runtimeVersion,
|
||||
buildProfile,
|
||||
gitCommitHash,
|
||||
gitCommitMessage,
|
||||
channel,
|
||||
message,
|
||||
} = build;
|
||||
const buildDate = new Date(updatedAt);
|
||||
|
||||
const splitCommitMessage = gitCommitMessage?.split('\n');
|
||||
const formattedCommitData =
|
||||
gitCommitHash && gitCommitMessage
|
||||
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(gitCommitMessage)}"`
|
||||
: 'Unknown';
|
||||
gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
|
||||
? `${gitCommitHash.slice(0, 7)} "${chalk.bold(
|
||||
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
|
||||
)}"`
|
||||
: null;
|
||||
|
||||
const title = `${chalk.bold(`ID:`)} ${id} (${chalk.bold(`${fromNow(buildDate)} ago`)})`;
|
||||
|
||||
const description = [
|
||||
`\t${chalk.bold(`Profile:`)} ${formatValue(buildProfile)}`,
|
||||
`\t${chalk.bold(`Channel:`)} ${formatValue(channel)}`,
|
||||
`\t${chalk.bold(`Runtime version:`)} ${formatValue(runtimeVersion)}`,
|
||||
`\t${chalk.bold(`Commit:`)} ${formattedCommitData}`,
|
||||
].join('\n');
|
||||
const descriptionItems: { name: string; value: string | null }[] = [
|
||||
{ name: 'Profile', value: buildProfile ? chalk.bold(buildProfile) : null },
|
||||
{ name: 'Channel', value: channel ? chalk.bold(channel) : null },
|
||||
{ name: 'Runtime version', value: runtimeVersion ? chalk.bold(runtimeVersion) : null },
|
||||
{ name: 'Commit', value: formattedCommitData },
|
||||
{
|
||||
name: 'Message',
|
||||
value: message
|
||||
? chalk.bold(message.length > 200 ? `${message.slice(0, 200)}...` : message)
|
||||
: null,
|
||||
},
|
||||
];
|
||||
|
||||
const filteredDescriptionArray: string[] = descriptionItems
|
||||
.filter(item => item.value)
|
||||
.map(item => `${chalk.bold(item.name)}: ${item.value}`);
|
||||
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
description: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
|
||||
value: build,
|
||||
disabled: buildDate < expiryDate,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { UpdateBranch, UpdateChannel } from '../../graphql/generated';
|
||||
import { Runtime, UpdateBranch, UpdateChannel } from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import { getBranchNameFromChannelNameAsync } from '../getBranchNameFromChannelNameAsync';
|
||||
|
||||
@@ -121,8 +121,10 @@ function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
|
||||
message: 'my message',
|
||||
createdAt: '2022-12-07T02:24:43.487Z',
|
||||
runtimeVersion: '1.0.0',
|
||||
runtime: {} as Runtime, // Temporary fix to resolve type errors
|
||||
platform: 'ios',
|
||||
manifestFragment: '...',
|
||||
isRollBackToEmbedded: false,
|
||||
activityTimestamp: '2022-12-07T02:24:43.487Z',
|
||||
branchId: '',
|
||||
awaitingCodeSigningInfo: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ import { resolveWorkflowPerPlatformAsync } from '../project/workflow';
|
||||
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from './android/UpdatesModule';
|
||||
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from './ios/UpdatesModule';
|
||||
|
||||
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'appVersion' } as const;
|
||||
export const DEFAULT_MANAGED_RUNTIME_VERSION = { policy: 'sdkVersion' } as const;
|
||||
export const DEFAULT_BARE_RUNTIME_VERSION = '1.0.0' as const;
|
||||
|
||||
function getDefaultRuntimeVersion(workflow: Workflow): NonNullable<ExpoConfig['runtimeVersion']> {
|
||||
@@ -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,
|
||||
|
||||
@@ -7,15 +7,25 @@ export type Actor = NonNullable<CurrentUserQuery['meActor']>;
|
||||
* This should be used whenever the "current user" needs to be displayed.
|
||||
* The display name CANNOT be used as project owner.
|
||||
*/
|
||||
export function getActorDisplayName(user?: Actor): string {
|
||||
switch (user?.__typename) {
|
||||
export function getActorDisplayName(actor?: Actor): string {
|
||||
switch (actor?.__typename) {
|
||||
case 'User':
|
||||
return user.username;
|
||||
return actor.username;
|
||||
case 'Robot':
|
||||
return user.firstName ? `${user.firstName} (robot)` : 'robot';
|
||||
return actor.firstName ? `${actor.firstName} (robot)` : 'robot';
|
||||
case 'SSOUser':
|
||||
return user.username ? `${user.username} (sso user)` : 'sso user';
|
||||
return actor.username ? `${actor.username} (sso user)` : 'sso user';
|
||||
default:
|
||||
return 'anonymous';
|
||||
}
|
||||
}
|
||||
|
||||
export function getActorUsername(actor?: Actor): string | null {
|
||||
switch (actor?.__typename) {
|
||||
case 'User':
|
||||
case 'SSOUser':
|
||||
return actor.username;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICyzCCAbOgAwIBAgIJWVXVdVgS9u+DMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
|
||||
BAMTBFRlc3QwHhcNMjIwMjA0MDEzODU2WhcNMjMwMjA0MDEzODU2WjAPMQ0wCwYD
|
||||
VQQDEwRUZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp1ILmaSl
|
||||
1pUVFgt/nLtzoaGuBW4OPRFN5uU2rYPow3uy4NfULKiir4AWxGETcm4gFIOG3RIb
|
||||
DJys6TAJJLnFEFnzpV+qaNeqaMqnXm4OBspx/WS2rSSGIwr7raKy4yrpPYJB1A2D
|
||||
5fZ6oFoWROyjbHJvPddu0PqbbEWSM3PzdZYOKfF13ofzFOPlsTnlo12QWgksxrcT
|
||||
wgeX7cjHsXaUc+I8U9DRR+9SgKhejWdFNPGcMkEc5BIXwtkTau+8+DwQzXz775Wl
|
||||
hf0eJwfnNQeleblbLQIp8i+RmfIjPSt8W/iTSN64iBROpOvnOeJ9gKX922QDZoqW
|
||||
z9o4RTNCPfyYBQIDAQABoyowKDAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAww
|
||||
CgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAHTyG2f/CReXVrS3zZjpgnje
|
||||
Vn2/XYevnaHNWYIxjlUVGaASE/k6qWkM832f0gaZwJYXsll2YxJa5Jcdqa1yq8QA
|
||||
Ay5YBq8Vv6K6iGTeZ5wTQc3xNys7zcS95eNq7yhDDjbLDbSDdZ8T/1hZqiha2DG5
|
||||
itLGfBH+HywW2G9njLgIzNcmKx3d76y3jlVXqug1/AVFK9in+r/0t0TYlk+opqZb
|
||||
H7cjJSefHusnq2vvvR6gbkZzyzSYP5FMlj/RjRCSJ+VHNwCn3fS+KDlJgVOk0Cxc
|
||||
7n52UGazeFwH3mem89DN+Bhw/uyU0IcIaDLykb6PsTPy2tjHsLpK5kpvfrp2m0o=
|
||||
-----END CERTIFICATE-----
|
||||
MIICzTCCAbWgAwIBAgIJWEL03IR99LhMMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV
|
||||
BAMTBHRlc3QwIBcNMjIwNDEyMTcyODIzWhgPMjEyMjA0MTIxNzI4MjNaMA8xDTAL
|
||||
BgNVBAMTBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBS7bz
|
||||
ELiUKOB8QNh0e0Oit7j7vk7ZICdyvBv+AB7PYbgImTvJSZ3CJTfmDYOt57ENd+ke
|
||||
V/RnD9b8u55ijroJBoR8QASVVsEYmgA1cv9owUYkIDxsAjxV9JZp3UgNeRmQ7IKI
|
||||
1J7myNnUQ3RIclSZkJO7ViHGSwTMXBanaO/OwjY6SM6Eca89XMcpOc/0xCBidFkO
|
||||
qn/jnIXk3uPAoLjPMQcX535+AUayevwSr6YqPV0zOGOYJy26n401eH3mu/UZveWy
|
||||
6qp7VumcBmz8AkvTJDgfSv04Pk3gpi4d8q4ABrtM46qWlw6UMMh5R6/i6Ou/8526
|
||||
xThOm3qEpEGXyZ2VAgMBAAGjKjAoMA4GA1UdDwEB/wQEAwIHgDAWBgNVHSUBAf8E
|
||||
DDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAQEATeE0FXXheYjw77pjy6sZ
|
||||
b6POytJdYLawMt7Chn11zP1TLx0LA1R01uH/ld0tVQQyt1XqG9AE3as1VCUYklPA
|
||||
zmwFSOxcSMiAXobEa5NcJ4S490YTjYmHI8fkz6TS+eeLkIr7eZMbpe6ck9V2d1e3
|
||||
0xHshYRc3NceNOKB018QYxbdxuTbV3a6f4yTlCSxKuyYfA5KDMoXaPAKE7Cwddrz
|
||||
1dbQpSI6djWknFjhTh7Bu/zpGWEx7wXTZlx9nIhKrinVowvyUkTaaCfuzdCmZOzS
|
||||
AJsADnJN0LEKDI4q0kDwl1v6U4cJ5Ru7VQv3xho03LPEA/fFKHK6OITDznspiXRs
|
||||
IA==
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,27 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAp1ILmaSl1pUVFgt/nLtzoaGuBW4OPRFN5uU2rYPow3uy4NfU
|
||||
LKiir4AWxGETcm4gFIOG3RIbDJys6TAJJLnFEFnzpV+qaNeqaMqnXm4OBspx/WS2
|
||||
rSSGIwr7raKy4yrpPYJB1A2D5fZ6oFoWROyjbHJvPddu0PqbbEWSM3PzdZYOKfF1
|
||||
3ofzFOPlsTnlo12QWgksxrcTwgeX7cjHsXaUc+I8U9DRR+9SgKhejWdFNPGcMkEc
|
||||
5BIXwtkTau+8+DwQzXz775Wlhf0eJwfnNQeleblbLQIp8i+RmfIjPSt8W/iTSN64
|
||||
iBROpOvnOeJ9gKX922QDZoqWz9o4RTNCPfyYBQIDAQABAoIBAEKMXVTEqbkJHpPg
|
||||
Cud5nuoAdkhul3cudL+LFw44UtG9V04aSadhgyMuXN/KhIOUzWmbTn6K/vsrLZKp
|
||||
qllTEdAJFuEFha+hZ4O6ZosmVqnYxzGzZvzCdB9n9OYAugmkPZRbRHdk0LscJ3Wz
|
||||
nvvW6sDtWtVL5CV2J28O1LFmQsTXPtEE5aQ5KX3Ix0OW1HBdEMOXRDFFDXhf7AKz
|
||||
nlvl3WRsXLyHYBfKs0pQHy8Gi+zl2OgKZDc9elMdU0Mx0jTJUUDtPSgNAsmdKO26
|
||||
qmkhbGd/kM1+6h+zngY3m48yD9elDxR3WUIYE2x5tT9fv3T6/xn1QfMz/gtToplx
|
||||
qY1DegECgYEA3bGUq91QMIH5rxrr2BBQpniXOTtOPAdvRcRh0/EsYhHcuL+uJEtG
|
||||
W9o9MqrREhXMMeD9Y8RorL+qefJjGq+GRwnPHlDjr6NkGTZfDKUn4IFH1xMbspET
|
||||
Nen5BsY5inUrQ/LSg1+HiVsSDyIJzGVcGMTOh9qC53M/zIGdiEwHgUECgYEAwTZ3
|
||||
AVieag5ETYahGdverxCRKlJ0MFJq3VKEdt6EcZHfeY1gmi17vGZ39q6djmfTT9Il
|
||||
QVP4/mxU2/mg/UgRC+rwiAZs3qlhOrtkrGd9qPOIeZ8PJkoNlvhM2WC2rGjTfXsb
|
||||
2IKtDEVXetwGgm3SXqpjycI5Xj6/9VlHAmCc4cUCgYAv9dT2AWDxvYyopyhSi+UG
|
||||
vpvok73vGqSl8UBAu7IgXUDk7wLbczV7dZE7vtyQDwsn10a6KKmEhcp5q0hpY4On
|
||||
JqYaJuG7A5wKIEsbzzb7SLyj+MxLKzt+tGldX9De9U4w2v1T0nzd6EfV4kVAZMUx
|
||||
zpHnrgwXykUJFxlffSM6gQKBgQCMxKXHsU0Zb/OLmD7fnDWNzsA02YYVfralMW2Z
|
||||
PV25cNIkuUBclC7GgNF+RJI+Ip7uVOkXw5pxo3PgIOuOHWduC2nbcPL49ucD52vd
|
||||
wDjpUyVnlt9uwh1MlPNInRH6YxVTItKS2AJEInEt7ghAFstidTnm0T8Czy0EEFuP
|
||||
+9vREQKBgQDIQxJ5oPIOEibJqn34z6qm5EmUyme/evvHxtQTaVuQi5CrzD5+qw1C
|
||||
jOTHmlJQQsb8b1g9wk7n+JbqWYhkRiAE6dwqvp/zDz1VfR90WbAPYQ8ZiXZ07D2P
|
||||
8P6ZXR3ZyDchuT6oUBXz5WtHUWOzXRJgPbXnFhrkIU8PkZYiPs/4Bw==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEAwUu28xC4lCjgfEDYdHtDore4+75O2SAncrwb/gAez2G4CJk7
|
||||
yUmdwiU35g2DreexDXfpHlf0Zw/W/LueYo66CQaEfEAElVbBGJoANXL/aMFGJCA8
|
||||
bAI8VfSWad1IDXkZkOyCiNSe5sjZ1EN0SHJUmZCTu1YhxksEzFwWp2jvzsI2OkjO
|
||||
hHGvPVzHKTnP9MQgYnRZDqp/45yF5N7jwKC4zzEHF+d+fgFGsnr8Eq+mKj1dMzhj
|
||||
mCctup+NNXh95rv1Gb3lsuqqe1bpnAZs/AJL0yQ4H0r9OD5N4KYuHfKuAAa7TOOq
|
||||
lpcOlDDIeUev4ujrv/OdusU4Tpt6hKRBl8mdlQIDAQABAoIBAEVijWCA/xDH/5T6
|
||||
nfhqCuRM+Mz9CkasYRyxY4bwuh6NIEeN4cUmdMetHnypGzyAr7B6+6ZVwjiAmhaM
|
||||
rpUIUMVOnp4PSNXml1fiZ/LHveD3h9sN60KGJuxf6OJFeUjE0KWSEGVXlVaYgIDO
|
||||
Wd5rk+yv1ifoCiWo5icJY2RiqbpYouveqb/InkQU+irZEwCviNvN1ZyJKqquNgcG
|
||||
FaY7iVd3WlHRealF1OfKHCBvSah9H5UBFWRBWUds2/xNzpCTAAGsmXdG6O9wPuEQ
|
||||
IhJqnSkTiQw49vyOH708VQnBfz/MILV7uTYqQ0Tep3+xkFFy4BSUIb6fE2zJegs5
|
||||
80MxIh0CgYEA4yf8cBcYidTAeoDbl2UXasDHAmNKeR4IxKAR3AJOiETIKDv4vAMP
|
||||
oBvHnUnnUQa5LhihrVXCbVwGqwATDka73K8sW/TkCE7OmuLDpZYvU/XQRWI74rka
|
||||
A/+nbD26NlwnM/a8mJNhbCVns/+PlLq1rK/5WlINcRNVk8PZ1UpG+nsCgYEA2dcM
|
||||
1TBZs1iqa+9AQ6nco5MWbsp3roq3sM06UneK0EGffWXPcmQkBZI9Jg+p5P2o/H13
|
||||
cYvkyKGmnL8qRZViXWEa/NI60kphQmFNGr+J/3PlKCOqZhlyqGhCFN9KLumJL17P
|
||||
XUuzVAa14UsfIFCiv7R33D02o/xw89u5TN+3ky8CgYEAhmkzszPHXk9YcWRsC2JS
|
||||
/+UAHQaZm59M+uPojXGD/JgOg9gwrzd0eH3XmNeRG13KF1+V01YKjOFGRMrve6QZ
|
||||
J1Uz/1mh0NSo5fdGY2XBrYGnclbVLqvum+0bqS3BUMcon1PhdrrGi9J6UYTn9c6h
|
||||
D4S1HF+u+njBS4U5ET85Tp8CgYEAno0bVf+/Cf467BxTFeyIHrZr2W+b9HoagKCf
|
||||
Fm7TpghdYRO9DXE9lqB9yToVWgoV+NAJI6fCeRTPA79PsR2tXnHTBris/2oLqBjR
|
||||
2eoXMsrTu4dZ+r4C6fgYQMDUaZiotMW5ABqdB0drEfNvUEHgcs+TfcVAA9M1EwiV
|
||||
shIStxkCgYBnxccKAfXywLjhVUCa50TsCrL8ToQ7WmWjU1LqN2iO/htFoxeIx4xP
|
||||
N7YauagBhyuypcq7SV3bKsdxlCzKi0cMNEonuRJb1Ejo3iU+4F7rzqsltGTjLtpB
|
||||
pgp5HP8sALzFZlWWif6W1TjzNJ87MIphr/VPcitRc9n4hWcbmxDvZA==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Loaded 100 of 115 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user