mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
306e7b1d71 | ||
|
|
a2b73844f0 | ||
|
|
ebbe786cd0 | ||
|
|
190172f06c | ||
|
|
2232a5ed59 | ||
|
|
ef3e9079e5 | ||
|
|
ec10eff1e6 | ||
|
|
3e3ec87622 | ||
|
|
f1cf92d63a | ||
|
|
7096daa116 | ||
|
|
6429e70a9a | ||
|
|
083381da82 | ||
|
|
1c1af2da86 | ||
|
|
72ce75e1ee | ||
|
|
ea553a1025 | ||
|
|
3a6bcb3f2e | ||
|
|
8b27255ce2 | ||
|
|
1f0ae27ea0 | ||
|
|
62d26ca32b | ||
|
|
3aebb54d11 | ||
|
|
0bf42e4cd4 | ||
|
|
ec4ef42f3c | ||
|
|
a70222d35d | ||
|
|
d20db0fff4 | ||
|
|
00ef6087f8 | ||
|
|
9c74c5c6b8 | ||
|
|
719febc519 | ||
|
|
9d9c99c0f6 | ||
|
|
e42031eaef | ||
|
|
9ec43bce84 | ||
|
|
488c3b3cb8 | ||
|
|
f5ac3d26a0 | ||
|
|
6be86d5119 | ||
|
|
04025802ac | ||
|
|
0a4c8785e6 |
No files matched your search
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"singleQuote": true
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
name: notify-slack-on-fail-or-recover
|
||||
|
||||
inputs:
|
||||
channel:
|
||||
required: true
|
||||
type: string
|
||||
slack_webhook:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Get previous workflow run status
|
||||
uses: actions/github-script@v6
|
||||
id: run-status
|
||||
with:
|
||||
script: |
|
||||
const script = require('.github/internal-actions/notify-slack-on-fail-or-recover/main.js');
|
||||
return await script({github, context, core});
|
||||
|
||||
- name: Commit subject
|
||||
shell: bash
|
||||
run: |
|
||||
subject="$(git show --format=%s --quiet | sed 's/"/\\"/g')"
|
||||
echo "${{ fromJSON(steps.run-status.outputs.result).previous }}"
|
||||
echo "SUBJECT=$subject" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Send Slack Success Notification
|
||||
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: ${{ inputs.channel }}
|
||||
SLACK_COLOR: good
|
||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
||||
SLACK_TITLE: 'Fixed, was failing'
|
||||
SLACK_MESSAGE: ${{ env.SUBJECT }}
|
||||
SLACK_USERNAME: 'EAS CLI CI'
|
||||
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
|
||||
SLACK_FOOTER: ''
|
||||
|
||||
- name: Send Slack Failure Notification
|
||||
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
|
||||
uses: rtCamp/action-slack-notify@v2
|
||||
env:
|
||||
SLACK_CHANNEL: ${{ inputs.channel }}
|
||||
SLACK_COLOR: danger
|
||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
||||
SLACK_TITLE: 'Failed'
|
||||
SLACK_MESSAGE: ${{ env.SUBJECT }}
|
||||
SLACK_USERNAME: 'EAS CLI CI'
|
||||
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
|
||||
SLACK_FOOTER: ''
|
||||
@@ -0,0 +1,155 @@
|
||||
function updateOctokitInstance(github) {
|
||||
updateGithubRestActions(github);
|
||||
addConvenienceMethods(github);
|
||||
}
|
||||
|
||||
function addConvenienceMethods(github) {
|
||||
Object.assign(github, {
|
||||
// checks if a run is a failure from the perspective of sending Slack
|
||||
// notifications. Works for both runs and run attempts, which should be the
|
||||
// same object shapes.
|
||||
// When a run has not been completed, it fetches the jobs for the workflow
|
||||
// to check if the job is "failing" instead of "failed".
|
||||
async isFailedRun(run) {
|
||||
console.log(`checking run: ${run.id}`);
|
||||
console.log(`status: ${run.status}`);
|
||||
console.log(`conclusion: ${run.conclusion}`);
|
||||
console.log(`run_attempt: ${run.run_attempt}`);
|
||||
|
||||
if (run.status === 'completed') {
|
||||
console.log('completed -- checking conclusion');
|
||||
return run.conclusion === 'failure' || run.conclusion === 'timed_out';
|
||||
}
|
||||
|
||||
const owner = run.repository.owner.login;
|
||||
const repo = run.repository.name;
|
||||
const jobsResp = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: run.id,
|
||||
});
|
||||
|
||||
console.log('checking jobs');
|
||||
const jobs = jobsResp.data;
|
||||
for (const job of jobs.jobs) {
|
||||
if (job.conclusion === 'failure' || job.conclusion === 'timed_out') {
|
||||
console.log('job failed');
|
||||
return true;
|
||||
} else if (job.status === 'completed') {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const step of job.steps) {
|
||||
if (step.conclusion === 'failure') {
|
||||
console.log('job step failed');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// github-script's Octokit/REST plugins are out of date, so we have to
|
||||
// add our own custom endpoint method
|
||||
// see https://octokit.github.io/rest.js/v18#custom-endpoint-methods for
|
||||
// documentation
|
||||
function updateGithubRestActions(github) {
|
||||
Object.assign(github.rest.actions, {
|
||||
getWorkflowRunAttempt: github.request.defaults({
|
||||
method: 'GET',
|
||||
url: '/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}',
|
||||
params: {
|
||||
owner: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
repo: {
|
||||
required: true,
|
||||
type: 'string',
|
||||
},
|
||||
run_id: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
},
|
||||
attempt_number: {
|
||||
required: true,
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
// Returns { current: 'failure' } | { current: 'success', previous: 'success' | 'failure' } to indicate the failure status
|
||||
// of the current and previous runs
|
||||
module.exports = async ({ github }) => {
|
||||
const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
|
||||
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
||||
updateOctokitInstance(github);
|
||||
|
||||
const workflowRunResp = await github.rest.actions.getWorkflowRun({
|
||||
owner,
|
||||
repo,
|
||||
run_id: GITHUB_RUN_ID,
|
||||
});
|
||||
|
||||
const currentRun = workflowRunResp.data;
|
||||
const listWorkflowRunsResp = await github.rest.actions.listWorkflowRuns({
|
||||
owner,
|
||||
repo,
|
||||
workflow_id: currentRun.workflow_id,
|
||||
branch: GITHUB_REF_NAME,
|
||||
});
|
||||
const { workflow_runs } = listWorkflowRunsResp.data;
|
||||
|
||||
if (workflow_runs.length === 1 && workflow_runs[0].run_attempt === 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const previousRuns = workflow_runs.filter(
|
||||
(run) => run.status === 'completed' && run.conclusion !== 'cancelled'
|
||||
);
|
||||
|
||||
if (await github.isFailedRun(currentRun)) {
|
||||
return { current: 'failure', previous: null };
|
||||
}
|
||||
|
||||
if (currentRun.run_attempt === 1 && previousRuns.length > 0) {
|
||||
return {
|
||||
current: 'success',
|
||||
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = currentRun.run_attempt - 1; i > 0; i--) {
|
||||
const previousAttemptResp = await github.rest.actions.getWorkflowRunAttempt({
|
||||
owner,
|
||||
repo,
|
||||
run_id: currentRun.id,
|
||||
attempt_number: i,
|
||||
});
|
||||
const previousAttempt = previousAttemptResp.data;
|
||||
if (
|
||||
previousAttempt.conclusion === 'success' ||
|
||||
previousAttempt.conclusion === 'failure' ||
|
||||
previousAttempt.conclusion === 'timed_out'
|
||||
) {
|
||||
return {
|
||||
current: 'success',
|
||||
previous: (await github.isFailedRun(previousAttempt)) ? 'failure' : 'success',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (previousRuns.length > 0) {
|
||||
return {
|
||||
current: 'success',
|
||||
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
|
||||
};
|
||||
}
|
||||
|
||||
return { current: 'success', previous: 'success' };
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "notify-slack-on-fail-or-recover",
|
||||
"version": "1.0.0",
|
||||
"description": "A GitHub Action to notify Expo Slack of build failures and recovery.",
|
||||
"main": "main.js",
|
||||
"author": "Expo",
|
||||
"license": "UNLICENSED",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"prettier": "^2.5.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
[email protected]:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
|
||||
@@ -36,10 +36,10 @@ jobs:
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- id: arm
|
||||
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
|
||||
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload linux-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
yarn pretarball-ci
|
||||
yarn oclif pack:tarballs --no-xz --targets darwin-x64
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||
- name: Upload darwin-x64 tarball
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -105,10 +105,10 @@ jobs:
|
||||
yarn oclif pack:win
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
- id: x64
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- id: x86
|
||||
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||
- name: Upload x64 installer
|
||||
uses: actions/upload-release-asset@v1
|
||||
@@ -147,12 +147,22 @@ jobs:
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
|
||||
echo "Run \"npm dist-tag add eas-cli@$VERSION latest-eas-build\" to promote it to production."
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
@@ -189,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 }}
|
||||
+154
@@ -8,6 +8,160 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Ensure public config is used as update. ([#1745](https://github.com/expo/eas-cli/pull/1745) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Unify how the command errors are displayed. ([#1738](https://github.com/expo/eas-cli/pull/1738) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
- Unify the case of base error messages for command failures. ([#1744](https://github.com/expo/eas-cli/pull/1744) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
|
||||
|
||||
## [3.8.0](https://github.com/expo/eas-cli/releases/tag/v3.8.0) - 2023-03-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add new `m-medium` and `m-large` resource classes. ([#1739](https://github.com/expo/eas-cli/pull/1739) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.7.2](https://github.com/expo/eas-cli/releases/tag/v3.7.2) - 2023-02-24
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Implement Apple's proprietary Hashcash algorithm for signing authentication requests. ([#1719](https://github.com/expo/eas-cli/pull/1719) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
|
||||
## [3.7.1](https://github.com/expo/eas-cli/releases/tag/v3.7.1) - 2023-02-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Bump `@expo/apple-utils` to fix Apple developer auth. ([03c76c1](https://github.com/expo/eas-cli/commit/03c76c1e0efb9ec55e40b45e33c80b3479f920c3) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [3.7.0](https://github.com/expo/eas-cli/releases/tag/v3.7.0) - 2023-02-23
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support variant detection for package*UniversalApk Gradle Commands. ([#1708](https://github.com/expo/eas-cli/pull/1708) by [@frw](https://github.com/frw))
|
||||
|
||||
## [3.6.1](https://github.com/expo/eas-cli/releases/tag/v3.6.1) - 2023-02-20
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Disable analytics when running behind a proxy. ([#1696](https://github.com/expo/eas-cli/pull/1696) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Clarify missing owner field error message for Robot users. ([#1702](https://github.com/expo/eas-cli/pull/1702) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Validate the platform for local builds earlier. ([#1698](https://github.com/expo/eas-cli/pull/1698) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Display build message when picking build in `eas submit`, `eas build:resign`, and `eas build:run` commands. ([#1700](https://github.com/expo/eas-cli/pull/1700) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Add a link directly to a build phase logs in the EAS CLI build error message. ([#1699](https://github.com/expo/eas-cli/pull/1699) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.6.0](https://github.com/expo/eas-cli/releases/tag/v3.6.0) - 2023-02-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Use `sdkVersion` as default runtime version policy when running `eas update:configure`. ([#1669](https://github.com/expo/eas-cli/pull/1669) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Warn when project ID but no owner specified and mismatch logged in user. ([#1667](https://github.com/expo/eas-cli/pull/1667) by [@wschurman](https://github.com/wschurman))
|
||||
- Return the build `message` if one is available. ([#1691](https://github.com/expo/eas-cli/pull/1691) by [@raulriera](https://github.com/raulriera))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix suggested application identifier to match owning account name. ([#1670](https://github.com/expo/eas-cli/pull/1670) by [@wschurman](https://github.com/wschurman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Add Xcode 14.2 image to the VSCode schema. ([#1693](https://github.com/expo/eas-cli/pull/1693) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.5.2](https://github.com/expo/eas-cli/releases/tag/v3.5.2) - 2023-02-05
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Use the new developer-mdn.apple.com subdomain instead of developer.apple.com. ([#1673](https://github.com/expo/eas-cli/pull/1673) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [3.5.1](https://github.com/expo/eas-cli/releases/tag/v3.5.1) - 2023-02-01
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix the issue with the `eas.json` envs being not available when resolving dynamic config. ([#1666](https://github.com/expo/eas-cli/pull/1666) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.5.0](https://github.com/expo/eas-cli/releases/tag/v3.5.0) - 2023-01-31
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Set `m1-medium` resource class for SDK version `>=48` and RN version `>=0.71` builds with unspecified resource class. ([#1655](https://github.com/expo/eas-cli/pull/1655) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix undefined branchMappingLogic. ([#1653](https://github.com/expo/eas-cli/pull/1653) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Fix using local credentials for internal distribution builds with universal provisioning. ([#1657](https://github.com/expo/eas-cli/pull/1657) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Move image validation to the server side and better handle server validation errors in the eas-cli. ([#1650](https://github.com/expo/eas-cli/pull/1650) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
- Update metadata on the worker when using git-based builds. ([#1651](https://github.com/expo/eas-cli/pull/1651) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.4.1](https://github.com/expo/eas-cli/releases/tag/v3.4.1) - 2023-01-23
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix incorrect exit code for successful local builds. ([#1649](https://github.com/expo/eas-cli/pull/1649) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
## [3.4.0](https://github.com/expo/eas-cli/releases/tag/v3.4.0) - 2023-01-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--json` flag to `eas config` command. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Add M1 resource class configuration to default eas.json when running eas build:configure on a new project. ([#1637](https://github.com/expo/eas-cli/pull/1637) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add prompt to switch iOS builds to M1 if the build queue is long. ([#1642](https://github.com/expo/eas-cli/pull/1642) by [@dsokal](https://github.com/dsokal))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix running commands with `--json` flag when EAS_NO_VCS is set. ([#1641](https://github.com/expo/eas-cli/pull/1641) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove unnecessary workaround for trailing backslash in .gitignore. ([#1622](https://github.com/expo/eas-cli/pull/1622) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Internal command that will be run on EAS worker when building from GitHub. ([#1568](https://github.com/expo/eas-cli/pull/1568) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
- Make the disabled tier error message more descriptive. ([#1635](https://github.com/expo/eas-cli/pull/1635) by [@dsokal](https://github.com/dsokal))
|
||||
- Control some of the EAS Build error messages server-side. ([#1639](https://github.com/expo/eas-cli/pull/1639) by [@wkozyra95](https://github.com/wkozyra95))
|
||||
|
||||
## [3.3.2](https://github.com/expo/eas-cli/releases/tag/v3.3.2) - 2023-01-12
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1619](https://github.com/expo/eas-cli/pull/1619) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Upgrade dependencies. ([#1617](https://github.com/expo/eas-cli/pull/1617) by [@dsokal](https://github.com/dsokal))
|
||||
- Improve handling `SSOUser` type. ([#1621](https://github.com/expo/eas-cli/pull/1621) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.3.1](https://github.com/expo/eas-cli/releases/tag/v3.3.1) - 2023-01-11
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert missing key information about updates in `eas update --json` and `eas update:view --json`. ([d20db0f](https://github.com/expo/eas-cli/commit/d20db0fff42a6f4512fea426a66cb6f168e37f7c) by [@ide](https://github.com/ide))
|
||||
|
||||
## [3.3.0](https://github.com/expo/eas-cli/releases/tag/v3.3.0) - 2023-01-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add ability to select resource class for a build in build profile. ([#1609](https://github.com/expo/eas-cli/pull/1609) by [@dsokal](https://github.com/dsokal))
|
||||
- Deprecate `--resource-class` flag. ([#1615](https://github.com/expo/eas-cli/pull/1615) by [@dsokal](https://github.com/dsokal))
|
||||
- Add new resource classes and update already available. ([#1616](https://github.com/expo/eas-cli/pull/1616) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Add missing key information about updates in `eas update --json` and `eas update:view --json`. ([#1611](https://github.com/expo/eas-cli/pull/1611) by [@byCedric](https://github.com/byCedric))
|
||||
|
||||
## [3.2.1](https://github.com/expo/eas-cli/releases/tag/v3.2.1) - 2023-01-09
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Validate chosen build in the `eas build:run` command. ([#1614](https://github.com/expo/eas-cli/pull/1614) by [@szdziedzic](https://github.com/szdziedzic))
|
||||
|
||||
## [3.2.0](https://github.com/expo/eas-cli/releases/tag/v3.2.0) - 2023-01-09
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--channel` option to `update:republish`. ([#1580](https://github.com/expo/eas-cli/pull/1580) by [@byCedric](https://github.com/byCedric))
|
||||
- Use `appVersion` as default runtime version policy when running `eas update:configure`. ([#1588](https://github.com/expo/eas-cli/pull/1588) by [@jonsamp](https://github.com/jonsamp))
|
||||
- Support `--json` flag in webhook list command. ([#1605](https://github.com/expo/eas-cli/pull/1605) by [@sheddy7](https://github.com/sheddy7))
|
||||
|
||||
@@ -12,3 +12,7 @@ The algorithm works as follows:
|
||||
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
|
||||
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
|
||||
- Otherwise, bump the PATCH version.
|
||||
|
||||
## Updating EAS CLI version on EAS Build
|
||||
|
||||
The release process promotes the new version to staging. After verifying it works there, you can promote it to production with `npm dist-tag add eas-cli@{version} latest-eas-build`.
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "3.2.0"
|
||||
"version": "3.8.1",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+13
-12
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "eas-cli-root",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
@@ -13,24 +14,24 @@
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.2.0",
|
||||
"@types/node": "18.11.5",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-universe": "11.1.0",
|
||||
"eslint-plugin-async-protect": "2.1.0",
|
||||
"@types/jest": "29.2.5",
|
||||
"@types/node": "18.11.18",
|
||||
"eslint": "8.31.0",
|
||||
"eslint-config-universe": "11.1.1",
|
||||
"eslint-plugin-async-protect": "3.0.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"jest": "29.2.2",
|
||||
"jest-watch-typeahead": "2.2.0",
|
||||
"lerna": "6.0.1",
|
||||
"oclif": "3.2.22",
|
||||
"prettier": "2.7.1",
|
||||
"jest": "29.3.1",
|
||||
"jest-watch-typeahead": "2.2.1",
|
||||
"lerna": "6.4.1",
|
||||
"oclif": "3.4.3",
|
||||
"prettier": "2.8.2",
|
||||
"ts-jest": "29.0.3",
|
||||
"typescript": "4.8.4"
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "18.6.0",
|
||||
|
||||
+59
-106
@@ -97,8 +97,6 @@ eas --help COMMAND
|
||||
* [`eas open`](#eas-open)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas run`](#eas-run)
|
||||
* [`eas run:run`](#eas-runrun)
|
||||
* [`eas secret:create`](#eas-secretcreate)
|
||||
* [`eas secret:delete`](#eas-secretdelete)
|
||||
* [`eas secret:list`](#eas-secretlist)
|
||||
@@ -132,7 +130,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -149,7 +147,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -166,7 +164,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -180,7 +178,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -209,7 +207,7 @@ EXAMPLES
|
||||
$ eas autocomplete --refresh-cache
|
||||
```
|
||||
|
||||
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.4/src/commands/autocomplete/index.ts)_
|
||||
_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v1.3.10/src/commands/autocomplete/index.ts)_
|
||||
|
||||
## `eas branch:create [NAME]`
|
||||
|
||||
@@ -230,7 +228,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -251,7 +249,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -271,7 +269,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -291,7 +289,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -314,7 +312,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -344,7 +342,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -361,7 +359,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -378,7 +376,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -413,7 +411,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -450,7 +448,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -476,7 +474,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -500,7 +498,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -547,7 +545,7 @@ DESCRIPTION
|
||||
Update version of an app.
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.2.0/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.8.1/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -566,7 +564,7 @@ DESCRIPTION
|
||||
Update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.2.0/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.8.1/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -583,7 +581,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -604,7 +602,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -626,7 +624,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -646,7 +644,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -669,7 +667,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -677,18 +675,20 @@ display project configuration (app.json + eas.json)
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas config [-p android|ios] [-e <value>]
|
||||
$ eas config [-p android|ios] [-e <value>] [--json --non-interactive]
|
||||
|
||||
FLAGS
|
||||
-e, --profile=PROFILE_NAME Name of the build profile from eas.json. Defaults to "production" if defined in
|
||||
eas.json.
|
||||
-p, --platform=(android|ios)
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
|
||||
DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -705,7 +705,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/credentials.ts)_
|
||||
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/credentials.ts)_
|
||||
|
||||
## `eas device:create`
|
||||
|
||||
@@ -719,7 +719,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -739,7 +739,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -760,7 +760,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -774,7 +774,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -788,7 +788,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -808,7 +808,7 @@ DESCRIPTION
|
||||
display help for eas-cli
|
||||
```
|
||||
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.16/src/commands/help.ts)_
|
||||
_See code: [@expo/plugin-help](https://github.com/expo/oclif-plugin-help/blob/v5.1.22/src/commands/help.ts)_
|
||||
|
||||
## `eas init`
|
||||
|
||||
@@ -876,7 +876,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -893,7 +893,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -910,7 +910,7 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas open`
|
||||
|
||||
@@ -924,7 +924,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -938,7 +938,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -960,55 +960,7 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/index.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/run/index.ts)_
|
||||
|
||||
## `eas run:run`
|
||||
|
||||
run simulator build
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas run:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios|all] [--offset
|
||||
<value>] [--limit <value>]
|
||||
|
||||
FLAGS
|
||||
-p, --platform=(android|ios|all)
|
||||
--id=<value> ID of the simulator build to run
|
||||
--latest Run the latest simulator build for specified platform
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 50 and is capped at 100.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--path=<value> Path to the simulator build file file
|
||||
--url=<value> Simulator build archive url
|
||||
|
||||
DESCRIPTION
|
||||
run simulator build
|
||||
```
|
||||
|
||||
_See code: [src/commands/run/run.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/run/run.ts)_
|
||||
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas secret:create`
|
||||
|
||||
@@ -1031,7 +983,7 @@ DESCRIPTION
|
||||
create an environment secret on the current project or owner account
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/secret/create.ts)_
|
||||
|
||||
## `eas secret:delete`
|
||||
|
||||
@@ -1049,7 +1001,7 @@ DESCRIPTION
|
||||
delete an environment secret by ID
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/secret/delete.ts)_
|
||||
|
||||
## `eas secret:list`
|
||||
|
||||
@@ -1063,7 +1015,7 @@ DESCRIPTION
|
||||
list environment secrets available for your current app
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/secret/list.ts)_
|
||||
|
||||
## `eas secret:push`
|
||||
|
||||
@@ -1083,7 +1035,7 @@ DESCRIPTION
|
||||
read environment secrets from env file and store on the server
|
||||
```
|
||||
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/secret/push.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1113,7 +1065,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1145,7 +1097,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1163,7 +1115,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1184,7 +1136,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1206,7 +1158,7 @@ DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1230,7 +1182,7 @@ DESCRIPTION
|
||||
rollback to an existing update
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -1250,7 +1202,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -1271,7 +1223,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -1291,7 +1243,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -1299,16 +1251,17 @@ list webhooks
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas webhook:list [--event BUILD|SUBMIT]
|
||||
$ eas webhook:list [--event BUILD|SUBMIT] [--json]
|
||||
|
||||
FLAGS
|
||||
--event=(BUILD|SUBMIT) Event type that triggers the webhook
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
|
||||
DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -1330,7 +1283,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -1347,7 +1300,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.2.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.8.1/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
|
||||
Generated
+5392
-2373
File diff suppressed because it is too large.
Load diff
@@ -1,39 +1,39 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "3.2.0",
|
||||
"version": "3.8.1",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||
"@expo/code-signing-certificates": "0.0.3",
|
||||
"@expo/config": "7.0.1",
|
||||
"@expo/config-plugins": "5.0.1",
|
||||
"@expo/config-types": "46.0.2",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"@expo/eas-json": "3.0.0",
|
||||
"@expo/json-file": "8.2.36",
|
||||
"@expo/apple-utils": "1.1.0",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "7.0.3",
|
||||
"@expo/config-plugins": "5.0.4",
|
||||
"@expo/config-types": "47.0.0",
|
||||
"@expo/eas-build-job": "0.2.106",
|
||||
"@expo/eas-json": "3.8.0",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "0.0.56",
|
||||
"@expo/package-manager": "0.0.57",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.18",
|
||||
"@expo/plugin-help": "5.1.16",
|
||||
"@expo/plugin-warn-if-update-available": "2.4.0",
|
||||
"@expo/prebuild-config": "5.0.3",
|
||||
"@expo/plist": "0.0.20",
|
||||
"@expo/plugin-help": "5.1.22",
|
||||
"@expo/plugin-warn-if-update-available": "2.5.0",
|
||||
"@expo/prebuild-config": "5.0.7",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "1.19.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.4",
|
||||
"@oclif/core": "1.23.2",
|
||||
"@oclif/plugin-autocomplete": "1.3.10",
|
||||
"@segment/ajv-human-errors": "^2.1.2",
|
||||
"@urql/core": "3.0.5",
|
||||
"@urql/core": "3.1.1",
|
||||
"@urql/exchange-retry": "1.0.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv": "8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"chalk": "4.1.2",
|
||||
@@ -52,14 +52,14 @@
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.2.0",
|
||||
"ignore": "5.2.4",
|
||||
"indent-string": "4.0.0",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.6.4",
|
||||
"joi": "17.7.0",
|
||||
"keychain": "1.3.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.1.0",
|
||||
"minimatch": "5.1.2",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
@@ -74,47 +74,47 @@
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.3.8",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.1.11",
|
||||
"tar": "6.1.13",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0",
|
||||
"tslib": "2.4.1",
|
||||
"turndown": "7.1.1",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.0",
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "2.13.7",
|
||||
"@graphql-codegen/introspection": "2.2.1",
|
||||
"@graphql-codegen/typescript": "2.8.0",
|
||||
"@graphql-codegen/typescript-operations": "2.5.5",
|
||||
"@graphql-codegen/cli": "2.16.3",
|
||||
"@graphql-codegen/introspection": "2.2.3",
|
||||
"@graphql-codegen/typescript": "2.8.7",
|
||||
"@graphql-codegen/typescript-operations": "2.5.12",
|
||||
"@types/cli-progress": "3.11.0",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/envinfo": "7.8.1",
|
||||
"@types/express": "4.17.14",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.3.0",
|
||||
"@types/node-forge": "1.3.1",
|
||||
"@types/pngjs": "6.0.1",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.4.1",
|
||||
"@types/semver": "7.3.12",
|
||||
"@types/prompts": "2.4.2",
|
||||
"@types/semver": "7.3.13",
|
||||
"@types/tar": "6.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@types/uuid": "9.0.0",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.18.2",
|
||||
"memfs": "3.4.7",
|
||||
"memfs": "3.4.13",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.2.9",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "4.0.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.8.4"
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -202,7 +202,8 @@
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rimraf oclif.manifest.json",
|
||||
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
|
||||
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildPriority,
|
||||
BuildResourceClass,
|
||||
BuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { confirmAsync, selectAsync } from '../../prompts';
|
||||
|
||||
@@ -85,5 +91,6 @@ function createMockBuildFragment({
|
||||
},
|
||||
},
|
||||
status: BuildStatus.InQueue,
|
||||
resourceClass: BuildResourceClass.AndroidMedium,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import {
|
||||
Android,
|
||||
ArchiveSource,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Job,
|
||||
Platform,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
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';
|
||||
@@ -41,11 +50,15 @@ export async function prepareJobAsync(
|
||||
}
|
||||
: {};
|
||||
|
||||
let buildType = ctx.buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||
let buildType = buildProfile.buildType;
|
||||
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') {
|
||||
buildType = Android.BuildType.APK;
|
||||
}
|
||||
|
||||
const maybeCustomBuildConfigPath = buildProfile.config
|
||||
? getCustomBuildConfigPath(buildProfile.config)
|
||||
: undefined;
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
@@ -67,8 +80,8 @@ export async function prepareJobAsync(
|
||||
secrets: {
|
||||
...buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
@@ -81,8 +94,15 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
prebuildCommand: buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
...(maybeCustomBuildConfigPath && {
|
||||
customBuildConfig: {
|
||||
path: maybeCustomBuildConfigPath,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
return sanitizeJob(job);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||
import { CredentialsSource } from '@expo/eas-json';
|
||||
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import cliProgress from 'cli-progress';
|
||||
import fs from 'fs-extra';
|
||||
@@ -9,15 +10,17 @@ 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,
|
||||
BuildParamsInput,
|
||||
BuildPriority,
|
||||
BuildResourceClass,
|
||||
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';
|
||||
@@ -26,13 +29,25 @@ import {
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { createProgressTracker } from '../utils/progress';
|
||||
import { sleepAsync } from '../utils/promise';
|
||||
import { getVcsClient } from '../vcs';
|
||||
import { BuildContext } from './context';
|
||||
import { runLocalBuildAsync } from './local';
|
||||
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';
|
||||
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
@@ -107,15 +122,24 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
);
|
||||
}
|
||||
|
||||
const projectArchive = ctx.localBuildOptions.enable
|
||||
? ({
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
} as const)
|
||||
: ({
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
} as const);
|
||||
let projectArchive: ArchiveSource | undefined;
|
||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: (await makeProjectTarballAsync()).path,
|
||||
};
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.PATH,
|
||||
path: process.cwd(),
|
||||
};
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
projectArchive = {
|
||||
type: ArchiveSourceType.GCS,
|
||||
bucketKey: await uploadProjectAsync(ctx),
|
||||
};
|
||||
}
|
||||
assert(projectArchive);
|
||||
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const buildParams = resolveBuildParamsInput(ctx);
|
||||
@@ -125,66 +149,55 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
});
|
||||
|
||||
return async () => {
|
||||
if (ctx.localBuildOptions.enable) {
|
||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else {
|
||||
} 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) {
|
||||
try {
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unknown localBuildMode.');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||
Log.error('EAS Build API has changed. Upgrade to the latest eas-cli version.');
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||
) {
|
||||
Log.error(
|
||||
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
|
||||
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
|
||||
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
|
||||
VALIDATION_ERROR: RequestValidationError,
|
||||
};
|
||||
|
||||
export function handleBuildRequestError(error: any, platform: Platform): never {
|
||||
Log.debug(JSON.stringify(error.graphQLErrors, null, 2));
|
||||
|
||||
const graphQLErrorCode: string = error?.graphQLErrors?.[0]?.extensions?.errorCode;
|
||||
if (graphQLErrorCode in SERVER_SIDE_DEFINED_ERRORS) {
|
||||
const ErrorClass: typeof EasCommandError = SERVER_SIDE_DEFINED_ERRORS[graphQLErrorCode];
|
||||
throw new ErrorClass(error?.graphQLErrors?.[0]?.message);
|
||||
} else if (graphQLErrorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE') {
|
||||
throw new EasBuildDownForMaintenanceError(
|
||||
`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_FREE_TIER_DISABLED') {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_IOS'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for iOS and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
throw new Error('Build request failed.');
|
||||
} else if (
|
||||
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID'
|
||||
) {
|
||||
Log.error(
|
||||
`EAS Build free tier is temporarily disabled for Android and we are not accepting any new builds. Try again later. ${learnMore(
|
||||
'https://expo.fyi/eas-build-queues'
|
||||
)}`
|
||||
);
|
||||
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.'
|
||||
);
|
||||
}
|
||||
@@ -285,7 +298,12 @@ export type MaybeBuildFragment = BuildFragment | null;
|
||||
|
||||
export async function waitForBuildEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||
{
|
||||
buildIds,
|
||||
accountName,
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
|
||||
{ intervalSec = 10 } = {}
|
||||
): Promise<MaybeBuildFragment[]> {
|
||||
let spinner;
|
||||
@@ -302,7 +320,10 @@ export async function waitForBuildEndAsync(
|
||||
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
|
||||
const { refetch } =
|
||||
builds.length === 1
|
||||
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
||||
? await handleSingleBuildProgressAsync(
|
||||
{ build: builds[0], accountName, projectDir, nonInteractive },
|
||||
{ spinner }
|
||||
)
|
||||
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
|
||||
if (!refetch) {
|
||||
return builds;
|
||||
@@ -337,7 +358,17 @@ const queueProgressBar = new cliProgress.SingleBar(
|
||||
);
|
||||
|
||||
async function handleSingleBuildProgressAsync(
|
||||
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
|
||||
{
|
||||
build,
|
||||
accountName,
|
||||
projectDir,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: MaybeBuildFragment;
|
||||
accountName: string;
|
||||
projectDir: string;
|
||||
nonInteractive: boolean;
|
||||
},
|
||||
{ spinner }: { spinner: Ora }
|
||||
): Promise<BuildProgressResult> {
|
||||
if (build === null) {
|
||||
@@ -376,7 +407,8 @@ async function handleSingleBuildProgressAsync(
|
||||
if (
|
||||
!queueProgressBarStarted &&
|
||||
typeof build.initialQueuePosition === 'number' &&
|
||||
typeof build.queuePosition === 'number'
|
||||
typeof build.queuePosition === 'number' &&
|
||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
||||
) {
|
||||
spinner.stopAndPersist();
|
||||
if (build.priority !== BuildPriority.High) {
|
||||
@@ -388,6 +420,55 @@ async function handleSingleBuildProgressAsync(
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
build.platform === AppPlatform.Ios &&
|
||||
[BuildResourceClass.IosIntelLarge, BuildResourceClass.IosIntelMedium].includes(
|
||||
build.resourceClass
|
||||
)
|
||||
) {
|
||||
let askToSwitchToM1 = false;
|
||||
if (
|
||||
build.priority === BuildPriority.High &&
|
||||
build.estimatedWaitTimeLeftSeconds >= /* 10 minutes */ 10 * 60
|
||||
) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`Warning: Priority queue wait time for legacy iOS Intel workers is longer than usual (more than 10 minutes).`
|
||||
);
|
||||
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
|
||||
Log.warn(
|
||||
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
|
||||
learnMoreMessage: 'Learn more on switching to M1 workers.',
|
||||
})
|
||||
);
|
||||
askToSwitchToM1 = true;
|
||||
} else if (
|
||||
build.priority !== BuildPriority.High &&
|
||||
build.estimatedWaitTimeLeftSeconds >= /* 120 minutes */ 120 * 60
|
||||
) {
|
||||
Log.newLine();
|
||||
Log.warn(
|
||||
`Warning: Free tier queue wait time for legacy iOS Intel workers is longer than usual.`
|
||||
);
|
||||
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
|
||||
Log.warn(
|
||||
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
|
||||
learnMoreMessage: 'Learn more on switching to M1 workers.',
|
||||
})
|
||||
);
|
||||
askToSwitchToM1 = true;
|
||||
}
|
||||
if (!nonInteractive && askToSwitchToM1) {
|
||||
const shouldSwitchToM1 = await confirmAsync({
|
||||
message: `Switch iOS builds to M1 workers (modifies build profiles in eas.json)?`,
|
||||
});
|
||||
if (shouldSwitchToM1) {
|
||||
await updateIosBuildProfilesToUseM1WorkersAsync(projectDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
|
||||
queueProgressBar.start(
|
||||
@@ -529,3 +610,21 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
|
||||
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||
await easJsonAccessor.readRawJsonAsync();
|
||||
|
||||
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
|
||||
easJsonAccessor.patch(easJsonRawObject => {
|
||||
for (const profileName of profileNames) {
|
||||
easJsonRawObject.build[profileName].ios = {
|
||||
...easJsonRawObject.build[profileName].ios,
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
};
|
||||
}
|
||||
return easJsonRawObject;
|
||||
});
|
||||
await easJsonAccessor.writeAsync();
|
||||
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
|
||||
import { EasJson, EasJsonAccessor, ResourceClass } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
@@ -53,11 +53,21 @@ const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
@@ -76,12 +86,20 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||
},
|
||||
ios: {
|
||||
buildConfiguration: 'Debug',
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {
|
||||
ios: {
|
||||
resourceClass: ResourceClass.M_MEDIUM,
|
||||
},
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
|
||||
@@ -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,11 +1,19 @@
|
||||
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
ArchiveSource,
|
||||
ArchiveSourceType,
|
||||
BuildTrigger,
|
||||
Metadata,
|
||||
Workflow,
|
||||
} from '@expo/eas-build-job';
|
||||
|
||||
import {
|
||||
BuildCredentialsSource,
|
||||
BuildIosEnterpriseProvisioning,
|
||||
BuildMetadataInput,
|
||||
BuildMode,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
BuildTrigger as GraphQLBuildTrigger,
|
||||
ProjectArchiveSourceInput,
|
||||
ProjectArchiveSourceType,
|
||||
} from '../graphql/generated';
|
||||
@@ -34,6 +42,7 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
buildMode: metadata.buildMode && transformBuildMode(metadata.buildMode),
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
@@ -81,3 +90,25 @@ export function transformIosEnterpriseProvisioning(
|
||||
return BuildIosEnterpriseProvisioning.Universal;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check what in metadata
|
||||
export function transformBuildMode(buildMode: string): BuildMode {
|
||||
if (buildMode === 'build') {
|
||||
return BuildMode.Build;
|
||||
} else if (buildMode === 'resign') {
|
||||
return BuildMode.Resign;
|
||||
} else if (buildMode === 'custom') {
|
||||
return BuildMode.Custom;
|
||||
} else {
|
||||
throw new Error(`Unsupported build mode: ${buildMode}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildTrigger {
|
||||
if (buildTrigger === BuildTrigger.EAS_CLI) {
|
||||
return GraphQLBuildTrigger.EasCli;
|
||||
} else if (buildTrigger === BuildTrigger.GIT_BASED_INTEGRATION) {
|
||||
return GraphQLBuildTrigger.GitBasedIntegration;
|
||||
}
|
||||
throw new Error('Unknown build trigger');
|
||||
}
|
||||
@@ -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: job.projectRootDirectory,
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||
import {
|
||||
ArchiveSource,
|
||||
BuildMode,
|
||||
BuildTrigger,
|
||||
Ios,
|
||||
Job,
|
||||
Platform,
|
||||
sanitizeJob,
|
||||
} from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import 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';
|
||||
@@ -25,10 +35,11 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Job> {
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
|
||||
if (jobData.credentials) {
|
||||
const targetNames = Object.keys(jobData.credentials);
|
||||
for (const targetName of targetNames) {
|
||||
@@ -36,38 +47,41 @@ export async function prepareJobAsync(
|
||||
}
|
||||
}
|
||||
|
||||
const maybeCustomBuildConfigPath = buildProfile.config
|
||||
? getCustomBuildConfigPath(buildProfile.config)
|
||||
: undefined;
|
||||
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
projectArchive: jobData.projectArchive,
|
||||
projectRootDirectory,
|
||||
builderEnvironment: {
|
||||
image: ctx.buildProfile.image,
|
||||
node: ctx.buildProfile.node,
|
||||
yarn: ctx.buildProfile.yarn,
|
||||
bundler: ctx.buildProfile.bundler,
|
||||
cocoapods: ctx.buildProfile.cocoapods,
|
||||
fastlane: ctx.buildProfile.fastlane,
|
||||
expoCli: ctx.buildProfile.expoCli,
|
||||
env: ctx.buildProfile.env,
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
yarn: buildProfile.yarn,
|
||||
bundler: buildProfile.bundler,
|
||||
cocoapods: buildProfile.cocoapods,
|
||||
fastlane: buildProfile.fastlane,
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
...ctx.buildProfile.cache,
|
||||
...buildProfile.cache,
|
||||
clear: ctx.clearCache,
|
||||
},
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||
updates: { channel: ctx.buildProfile.channel },
|
||||
developmentClient: ctx.buildProfile.developmentClient,
|
||||
simulator: ctx.buildProfile.simulator,
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
simulator: buildProfile.simulator,
|
||||
scheme: jobData.buildScheme,
|
||||
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||
applicationArchivePath:
|
||||
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
|
||||
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
|
||||
buildConfiguration: buildProfile.buildConfiguration,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
username,
|
||||
...(ctx.ios.buildNumberOverride && {
|
||||
version: {
|
||||
@@ -75,8 +89,15 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
experimental: {
|
||||
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||
prebuildCommand: buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
...(maybeCustomBuildConfigPath && {
|
||||
customBuildConfig: {
|
||||
path: maybeCustomBuildConfigPath,
|
||||
},
|
||||
}),
|
||||
};
|
||||
return sanitizeJob(job);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,28 @@ import semver from 'semver';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.120';
|
||||
const PLUGIN_PACKAGE_VERSION = '0.0.128';
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
* Local build that users can run on their own machines. Instead
|
||||
* of sending build request to EAS Servers it's passing it as an argument
|
||||
* to local-build-plugin, that will run the build locally.
|
||||
*
|
||||
* Triggered when running `eas build --local`.
|
||||
*/
|
||||
LOCAL_BUILD_PLUGIN = 'local-build-plugin',
|
||||
/**
|
||||
* Type of local build that is not accessible to users directly. When
|
||||
* cloud build is triggered by git based integration, we are running
|
||||
* in this mode. Instead of sending build request to EAS Servers it's
|
||||
* printing it to the stdout as JSON, so EAS Build worker can read it.
|
||||
*/
|
||||
INTERNAL = 'internal',
|
||||
}
|
||||
|
||||
export interface LocalBuildOptions {
|
||||
enable: boolean;
|
||||
localBuildMode?: LocalBuildMode;
|
||||
skipCleanup?: boolean;
|
||||
skipNativeBuild?: boolean;
|
||||
artifactsDir?: string;
|
||||
|
||||
@@ -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:');
|
||||
|
||||
@@ -74,11 +74,11 @@ export async function listAndSelectBuildOnAppAsync(
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
|
||||
selectPromptWarningMessage?: string;
|
||||
}
|
||||
): Promise<BuildFragment | void> {
|
||||
): Promise<BuildFragment | null> {
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
throw new Error('Unable to select a build in non-interactive mode.');
|
||||
} else {
|
||||
return await paginatedQueryWithSelectPromptAsync({
|
||||
const selectedBuild = await paginatedQueryWithSelectPromptAsync({
|
||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
@@ -95,13 +95,10 @@ export async function listAndSelectBuildOnAppAsync(
|
||||
selectPromptWarningMessage,
|
||||
},
|
||||
});
|
||||
return selectedBuild ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBuildChoiceValue(value: string | undefined | null): string {
|
||||
return value ? chalk.bold(value) : 'Unknown';
|
||||
}
|
||||
|
||||
function createBuildToPartialChoiceMaker(
|
||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
|
||||
) {
|
||||
@@ -118,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),
|
||||
};
|
||||
};
|
||||
@@ -145,7 +157,7 @@ export async function getLatestBuildAsync(
|
||||
projectId: string;
|
||||
filter?: BuildFilter;
|
||||
}
|
||||
): Promise<BuildFragment> {
|
||||
): Promise<BuildFragment | null> {
|
||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
limit: 1,
|
||||
@@ -154,7 +166,7 @@ export async function getLatestBuildAsync(
|
||||
});
|
||||
|
||||
if (builds.length === 0) {
|
||||
throw new Error('Found no build matching the provided criteria.');
|
||||
return null;
|
||||
}
|
||||
|
||||
return builds[0];
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
EasJson,
|
||||
EasJsonAccessor,
|
||||
EasJsonUtils,
|
||||
ResourceClass,
|
||||
SubmitProfile,
|
||||
} from '@expo/eas-json';
|
||||
import assert from 'assert';
|
||||
@@ -17,7 +18,6 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildResourceClass,
|
||||
BuildStatus,
|
||||
BuildWithSubmissionsFragment,
|
||||
SubmissionFragment,
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
appPlatformEmojis,
|
||||
toPlatforms,
|
||||
} from '../platform';
|
||||
import { validateCustomBuildConfigAsync } from '../project/customBuildConfig';
|
||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
|
||||
@@ -62,7 +63,6 @@ import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { UserInputResourceClass } from './types';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
||||
@@ -80,47 +80,10 @@ export interface BuildFlags {
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
userInputResourceClass?: UserInputResourceClass;
|
||||
resourceClass?: ResourceClass;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const iosUserInputResourceClassToBuildResourceClassMapping: Record<
|
||||
UserInputResourceClass,
|
||||
BuildResourceClass
|
||||
> = {
|
||||
[UserInputResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||
[UserInputResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||
[UserInputResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosM1Large,
|
||||
};
|
||||
|
||||
const androidUserInputResourceClassToBuildResourceClassMapping: Record<
|
||||
Exclude<UserInputResourceClass, UserInputResourceClass.M1_EXPERIMENTAL>,
|
||||
BuildResourceClass
|
||||
> = {
|
||||
[UserInputResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||
[UserInputResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||
};
|
||||
|
||||
function resolveResourceClass(
|
||||
platform: Platform,
|
||||
resourceClassInput: UserInputResourceClass
|
||||
): BuildResourceClass {
|
||||
if (platform !== Platform.IOS && resourceClassInput === UserInputResourceClass.M1_EXPERIMENTAL) {
|
||||
throw new Error(
|
||||
`Resource class ${UserInputResourceClass.M1_EXPERIMENTAL} is only available for iOS builds`
|
||||
);
|
||||
}
|
||||
|
||||
return platform === Platform.ANDROID
|
||||
? androidUserInputResourceClassToBuildResourceClassMapping[
|
||||
resourceClassInput as Exclude<
|
||||
UserInputResourceClass,
|
||||
UserInputResourceClass.M1_EXPERIMENTAL
|
||||
>
|
||||
]
|
||||
: iosUserInputResourceClassToBuildResourceClassMapping[resourceClassInput];
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
@@ -156,6 +119,7 @@ export async function runBuildAndSubmitAsync(
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||
await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
|
||||
}
|
||||
|
||||
const startedBuilds: {
|
||||
@@ -170,10 +134,6 @@ export async function runBuildAndSubmitAsync(
|
||||
flags,
|
||||
moreBuilds: platforms.length > 1,
|
||||
buildProfile,
|
||||
resourceClass: resolveResourceClass(
|
||||
buildProfile.platform,
|
||||
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
|
||||
),
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
@@ -186,7 +146,7 @@ export async function runBuildAndSubmitAsync(
|
||||
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
if (flags.localBuildOptions.localBuildMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -240,6 +200,8 @@ export async function runBuildAndSubmitAsync(
|
||||
const builds = await waitForBuildEndAsync(graphqlClient, {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
accountName,
|
||||
projectDir,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
});
|
||||
if (!flags.json) {
|
||||
printBuildResults(builds);
|
||||
@@ -279,7 +241,6 @@ async function prepareAndStartBuildAsync({
|
||||
flags,
|
||||
moreBuilds,
|
||||
buildProfile,
|
||||
resourceClass,
|
||||
easJsonCliConfig,
|
||||
actor,
|
||||
graphqlClient,
|
||||
@@ -290,7 +251,6 @@ async function prepareAndStartBuildAsync({
|
||||
flags: BuildFlags;
|
||||
moreBuilds: boolean;
|
||||
buildProfile: ProfileData<'build'>;
|
||||
resourceClass: BuildResourceClass;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
actor: Actor;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
@@ -299,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,
|
||||
|
||||
@@ -12,13 +12,3 @@ export enum BuildDistributionType {
|
||||
INTERNAL = 'internal',
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export enum UserInputResourceClass {
|
||||
DEFAULT = 'default',
|
||||
LARGE = 'large',
|
||||
/**
|
||||
* @experimental
|
||||
* This resource class is not yet ready to be used in production. For testing purposes only. Might be deprecated / deleted at any time.
|
||||
*/
|
||||
M1_EXPERIMENTAL = 'm1-experimental',
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+73
-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'
|
||||
)}`
|
||||
);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -93,14 +93,40 @@ export async function getProjectIdAsync(
|
||||
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" (${
|
||||
@@ -132,6 +158,8 @@ export async function getProjectIdAsync(
|
||||
throw new Error(
|
||||
'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'
|
||||
);
|
||||
case 'SSOUser':
|
||||
throw new Error('SSO users are not supported yet.');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
import { EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
|
||||
import { Errors, Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { LocalBuildMode } from '../../build/local';
|
||||
import { BuildFlags, runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import { UserInputResourceClass } from '../../build/types';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { StatuspageServiceName } from '../../graphql/generated';
|
||||
@@ -31,7 +31,7 @@ interface RawBuildFlags {
|
||||
json: boolean;
|
||||
'auto-submit': boolean;
|
||||
'auto-submit-with-profile'?: string;
|
||||
'resource-class'?: UserInputResourceClass;
|
||||
'resource-class'?: ResourceClass;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@@ -85,8 +85,13 @@ export default class Build extends EasCommand {
|
||||
exclusive: ['auto-submit'],
|
||||
}),
|
||||
'resource-class': Flags.enum({
|
||||
options: Object.values(UserInputResourceClass),
|
||||
options: Object.values(ResourceClass),
|
||||
hidden: true,
|
||||
deprecated: {
|
||||
message: chalk.yellow(
|
||||
'The --resource-class flag has been deprecated. Define the resource class in eas.json.\nLearn more: https://docs.expo.dev/build-reference/eas-json/'
|
||||
),
|
||||
},
|
||||
description: 'The instance type that will be used to run this build [experimental]',
|
||||
}),
|
||||
message: Flags.string({
|
||||
@@ -122,7 +127,7 @@ export default class Build extends EasCommand {
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
if (!flags.localBuildOptions.enable) {
|
||||
if (!flags.localBuildOptions.localBuildMode) {
|
||||
await maybeWarnAboutEasOutagesAsync(
|
||||
graphqlClient,
|
||||
flags.autoSubmit
|
||||
@@ -156,17 +161,6 @@ export default class Build extends EasCommand {
|
||||
if (flags.json && !nonInteractive) {
|
||||
Errors.error('--json is allowed only when building in non-interactive mode', { exit: 1 });
|
||||
}
|
||||
if (
|
||||
flags['resource-class'] === UserInputResourceClass.M1_EXPERIMENTAL &&
|
||||
flags.platform !== Platform.IOS
|
||||
) {
|
||||
Errors.error(
|
||||
`Resource class ${UserInputResourceClass.M1_EXPERIMENTAL} is only available for iOS builds`,
|
||||
{
|
||||
exit: 1,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const requestedPlatform =
|
||||
flags.platform &&
|
||||
@@ -201,19 +195,17 @@ export default class Build extends EasCommand {
|
||||
nonInteractive,
|
||||
localBuildOptions: flags['local']
|
||||
? {
|
||||
enable: true,
|
||||
localBuildMode: LocalBuildMode.LOCAL_BUILD_PLUGIN,
|
||||
verbose: true,
|
||||
artifactPath: flags.output && path.resolve(process.cwd(), flags.output),
|
||||
}
|
||||
: {
|
||||
enable: false,
|
||||
},
|
||||
: {},
|
||||
wait: flags['wait'],
|
||||
clearCache: flags['clear-cache'],
|
||||
json: flags['json'],
|
||||
autoSubmit: flags['auto-submit'] || flags['auto-submit-with-profile'] !== undefined,
|
||||
submitProfile: flags['auto-submit-with-profile'] ?? profile,
|
||||
userInputResourceClass: flags['resource-class'] ?? UserInputResourceClass.DEFAULT,
|
||||
resourceClass: flags['resource-class'],
|
||||
message,
|
||||
};
|
||||
}
|
||||
@@ -223,7 +215,7 @@ export default class Build extends EasCommand {
|
||||
): Promise<BuildFlags> {
|
||||
const requestedPlatform = await selectRequestedPlatformAsync(flags.requestedPlatform);
|
||||
|
||||
if (flags.localBuildOptions.enable) {
|
||||
if (flags.localBuildOptions.localBuildMode) {
|
||||
if (flags.autoSubmit) {
|
||||
// TODO: implement this
|
||||
Errors.error('Auto-submits are not yet supported when building locally', { exit: 1 });
|
||||
@@ -235,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { LocalBuildMode } from '../../build/local';
|
||||
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import Log from '../../log';
|
||||
@@ -110,7 +111,7 @@ export default class BuildInspect extends EasCommand {
|
||||
requestedPlatform: flags.platform,
|
||||
profile: flags.profile,
|
||||
localBuildOptions: {
|
||||
enable: true,
|
||||
localBuildMode: LocalBuildMode.LOCAL_BUILD_PLUGIN,
|
||||
...(flags.stage === InspectStage.PRE_BUILD ? { skipNativeBuild: true } : {}),
|
||||
...(flags.stage === InspectStage.POST_BUILD ? { skipCleanup: true } : {}),
|
||||
verbose: flags.verbose,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { handleDeprecatedEasJsonAsync } from '.';
|
||||
import { LocalBuildMode } from '../../build/local';
|
||||
import { runBuildAndSubmitAsync } from '../../build/runBuildAndSubmit';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
|
||||
/**
|
||||
* This command will be run on the EAS Build workers, when building
|
||||
* directly from git. This command resolves credentials and other
|
||||
* build configuration, that normally would be included in the
|
||||
* job and metadata objects, and prints them to stdout.
|
||||
*/
|
||||
export default class BuildInternal extends EasCommand {
|
||||
static override hidden = true;
|
||||
|
||||
static override flags = {
|
||||
platform: Flags.enum({
|
||||
char: 'p',
|
||||
options: ['android', 'ios'],
|
||||
required: true,
|
||||
}),
|
||||
profile: Flags.string({
|
||||
char: 'e',
|
||||
description:
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Analytics,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(BuildInternal);
|
||||
// This command is always run with implicit --non-interactive and --json options
|
||||
enableJsonOutput();
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
getDynamicProjectConfigAsync,
|
||||
projectDir,
|
||||
analytics,
|
||||
} = await this.getContextAsync(BuildInternal, {
|
||||
nonInteractive: true,
|
||||
});
|
||||
|
||||
await handleDeprecatedEasJsonAsync(projectDir, flags.nonInteractive);
|
||||
|
||||
await runBuildAndSubmitAsync(
|
||||
graphqlClient,
|
||||
analytics,
|
||||
projectDir,
|
||||
{
|
||||
requestedPlatform: flags.platform as RequestedPlatform,
|
||||
profile: flags.profile,
|
||||
nonInteractive: true,
|
||||
wait: false,
|
||||
clearCache: false,
|
||||
json: true,
|
||||
autoSubmit: false,
|
||||
localBuildOptions: {
|
||||
localBuildMode: LocalBuildMode.INTERNAL,
|
||||
},
|
||||
},
|
||||
actor,
|
||||
getDynamicProjectConfigAsync
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,8 @@ export default class BuildResign extends EasCommand {
|
||||
const buildResult = await waitForBuildEndAsync(graphqlClient, {
|
||||
buildIds: [newBuild.id],
|
||||
accountName: account.name,
|
||||
nonInteractive,
|
||||
projectDir,
|
||||
});
|
||||
if (!flags.json) {
|
||||
printBuildResults(buildResult);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { AppPlatform, BuildFragment, BuildStatus, DistributionType } from '../../graphql/generated';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import Log from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { RunArchiveFlags, runAsync } from '../../run/run';
|
||||
@@ -150,6 +151,35 @@ async function resolvePlatformAsync(platform?: string): Promise<AppPlatform> {
|
||||
return selectedPlatform;
|
||||
}
|
||||
|
||||
function validateChosenBuild(
|
||||
maybeBuild: BuildFragment | null,
|
||||
selectedPlatform: AppPlatform
|
||||
): BuildFragment {
|
||||
if (!maybeBuild) {
|
||||
throw new Error('There are no simulator/emulator builds that can be run for this project.');
|
||||
}
|
||||
|
||||
if (selectedPlatform !== maybeBuild.platform) {
|
||||
throw new Error(
|
||||
`The selected build is for ${
|
||||
appPlatformDisplayNames[maybeBuild.platform]
|
||||
}, but you selected ${appPlatformDisplayNames[selectedPlatform]}`
|
||||
);
|
||||
}
|
||||
|
||||
if (maybeBuild.status !== BuildStatus.Finished) {
|
||||
throw new Error('The selected build is not finished.');
|
||||
}
|
||||
|
||||
if (!isRunnableOnSimulatorOrEmulator(maybeBuild)) {
|
||||
throw new Error(
|
||||
'Artifacts for the latest build have expired and are no longer available, or this is not a simulator/emulator build.'
|
||||
);
|
||||
}
|
||||
|
||||
return maybeBuild;
|
||||
}
|
||||
|
||||
async function maybeGetBuildAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
flags: RunCommandFlags,
|
||||
@@ -160,7 +190,8 @@ async function maybeGetBuildAsync(
|
||||
flags.selectedPlatform === AppPlatform.Ios ? DistributionType.Simulator : undefined;
|
||||
|
||||
if (flags.runArchiveFlags.id) {
|
||||
return BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
|
||||
const build = await BuildQuery.byIdAsync(graphqlClient, flags.runArchiveFlags.id);
|
||||
return validateChosenBuild(build, flags.selectedPlatform);
|
||||
} else if (
|
||||
!flags.runArchiveFlags.id &&
|
||||
!flags.runArchiveFlags.path &&
|
||||
@@ -169,7 +200,7 @@ async function maybeGetBuildAsync(
|
||||
) {
|
||||
const build = await listAndSelectBuildOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
title: `Select ${flags.selectedPlatform === AppPlatform.Ios ? 'iOS' : 'Android'} ${
|
||||
title: `Select ${appPlatformDisplayNames[flags.selectedPlatform]} ${
|
||||
flags.selectedPlatform === AppPlatform.Ios ? 'simulator' : 'emulator'
|
||||
} build to run for ${await getDisplayNameForProjectIdAsync(graphqlClient, projectId)} app`,
|
||||
filter: {
|
||||
@@ -182,10 +213,9 @@ async function maybeGetBuildAsync(
|
||||
selectPromptWarningMessage:
|
||||
'Artifacts for this build have expired and are no longer available, or this is not a simulator/emulator build.',
|
||||
});
|
||||
|
||||
return build ?? null;
|
||||
return validateChosenBuild(build, flags.selectedPlatform);
|
||||
} else if (flags.runArchiveFlags.latest) {
|
||||
return await getLatestBuildAsync(graphqlClient, {
|
||||
const latestBuild = await getLatestBuildAsync(graphqlClient, {
|
||||
projectId,
|
||||
filter: {
|
||||
platform: flags.selectedPlatform,
|
||||
@@ -193,6 +223,8 @@ async function maybeGetBuildAsync(
|
||||
status: BuildStatus.Finished,
|
||||
},
|
||||
});
|
||||
|
||||
return validateChosenBuild(latestBuild, flags.selectedPlatform);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
@@ -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, {
|
||||
@@ -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,
|
||||
|
||||
@@ -5,10 +5,12 @@ import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import EasCommand from '../commandUtils/EasCommand';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../commandUtils/flags';
|
||||
import { toAppPlatform } from '../graphql/types/AppPlatform';
|
||||
import Log from '../log';
|
||||
import { appPlatformEmojis } from '../platform';
|
||||
import { selectAsync } from '../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../utils/json';
|
||||
|
||||
export default class Config extends EasCommand {
|
||||
static override description = 'display project configuration (app.json + eas.json)';
|
||||
@@ -21,6 +23,11 @@ export default class Config extends EasCommand {
|
||||
'Name of the build profile from eas.json. Defaults to "production" if defined in eas.json.',
|
||||
helpValue: 'PROFILE_NAME',
|
||||
}),
|
||||
// This option is used only on EAS Build worker to read build profile from eas.json.
|
||||
'eas-json-only': Flags.boolean({
|
||||
hidden: true,
|
||||
}),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
@@ -30,6 +37,9 @@ export default class Config extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(Config);
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
const { platform: maybePlatform, profile: maybeProfile } = flags;
|
||||
const { getDynamicProjectConfigAsync, projectDir } = await this.getContextAsync(Config, {
|
||||
nonInteractive: false,
|
||||
@@ -61,21 +71,37 @@ export default class Config extends EasCommand {
|
||||
]));
|
||||
|
||||
const profile = await EasJsonUtils.getBuildProfileAsync(accessor, platform, profileName);
|
||||
const { exp: config } = await getDynamicProjectConfigAsync({
|
||||
env: profile.env,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
if (flags['eas-json-only']) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput({ buildProfile: profile });
|
||||
} else {
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const platformEmoji = appPlatformEmojis[appPlatform];
|
||||
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
} else {
|
||||
const { exp: appConfig } = await getDynamicProjectConfigAsync({
|
||||
env: profile.env,
|
||||
isPublicConfig: true,
|
||||
});
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(config, null, 2));
|
||||
Log.newLine();
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const platformEmoji = appPlatformEmojis[appPlatform];
|
||||
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput({ buildProfile: profile, appConfig });
|
||||
} else {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold(getProjectConfigDescription(projectDir)));
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(appConfig, null, 2));
|
||||
Log.newLine();
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(platform);
|
||||
const platformEmoji = appPlatformEmojis[appPlatform];
|
||||
Log.log(`${platformEmoji} ${chalk.bold(`Build profile "${profileName}"`)}`);
|
||||
Log.newLine();
|
||||
Log.log(JSON.stringify(profile, null, 2));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -243,7 +238,9 @@ export default class ProjectInit extends EasCommand {
|
||||
const sortedAccounts =
|
||||
actor.__typename === 'Robot'
|
||||
? allAccounts
|
||||
: [...allAccounts].sort((a, _b) => (a.name === actor.username ? -1 : 1));
|
||||
: [...allAccounts].sort((a, _b) =>
|
||||
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
|
||||
);
|
||||
|
||||
const choices = sortedAccounts.map(account => ({
|
||||
title: account.name,
|
||||
@@ -285,7 +282,7 @@ export default class ProjectInit extends EasCommand {
|
||||
}
|
||||
|
||||
await ProjectInit.saveProjectIdAndLogSuccessAsync(projectDir, existingProjectIdOnServer);
|
||||
return;
|
||||
return existingProjectIdOnServer;
|
||||
}
|
||||
|
||||
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
|
||||
@@ -321,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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
import { AppJSONConfig, PackageJSONConfig, Platform, getConfig } from '@expo/config';
|
||||
import {
|
||||
AppJSONConfig,
|
||||
ExpoConfig,
|
||||
GetConfigOptions,
|
||||
PackageJSONConfig,
|
||||
Platform,
|
||||
getConfig,
|
||||
} from '@expo/config';
|
||||
import { Updates } from '@expo/config-plugins';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
@@ -12,6 +19,7 @@ import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/cr
|
||||
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 { collectAssetsAsync, uploadAssetsAsync } from '../../../project/publish';
|
||||
@@ -19,6 +27,19 @@ import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchName
|
||||
|
||||
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' }),
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('@expo/config');
|
||||
@@ -32,7 +53,7 @@ jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../graphql/queries/UpdateQuery');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: () => ({
|
||||
start: () => ({ succeed: () => {}, fail: () => {} }),
|
||||
start: () => ({ succeed: () => {}, fail: () => {}, stop: () => {} }),
|
||||
}),
|
||||
}));
|
||||
jest.mock('../../../project/publish', () => ({
|
||||
@@ -44,7 +65,10 @@ jest.mock('../../../project/publish', () => ({
|
||||
}));
|
||||
|
||||
describe(UpdatePublish.name, () => {
|
||||
afterEach(() => vol.reset());
|
||||
afterEach(() => {
|
||||
vol.reset();
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockClear();
|
||||
});
|
||||
|
||||
// Deprecated and split to a new command: update:republish
|
||||
it('errors with --republish', async () => {
|
||||
@@ -81,15 +105,9 @@ describe(UpdatePublish.name, () => {
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
id: 'update123',
|
||||
group: 'group123',
|
||||
runtimeVersion,
|
||||
platform,
|
||||
manifestPermalink: 'https://example.com/update123/manifest',
|
||||
}))
|
||||
);
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
@@ -110,11 +128,9 @@ describe(UpdatePublish.name, () => {
|
||||
|
||||
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue(
|
||||
platforms.map(platform => ({
|
||||
id: 'update123',
|
||||
group: 'group123',
|
||||
...updateStub,
|
||||
runtimeVersion,
|
||||
platform,
|
||||
manifestPermalink: 'https://example.com/update123/manifest',
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -130,14 +146,59 @@ describe(UpdatePublish.name, () => {
|
||||
|
||||
expect(PublishMutation.publishUpdateGroupAsync).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('creates a new update with the public expo config', async () => {
|
||||
const flags = ['--non-interactive', '--branch=branch123', '--message=abc'];
|
||||
|
||||
// Add configuration to the project that should not be included in the update
|
||||
const { appJson } = mockTestProject({
|
||||
expoConfig: {
|
||||
hooks: {
|
||||
postPublish: [
|
||||
{
|
||||
file: 'custom-hook.js',
|
||||
config: { some: 'config' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { platforms, runtimeVersion } = mockTestExport({ platforms: ['ios'] });
|
||||
|
||||
// Mock an existing branch, so we don't create a new one
|
||||
jest.mocked(ensureBranchExistsAsync).mockResolvedValue({
|
||||
branchId: 'branch123',
|
||||
createdBranch: false,
|
||||
});
|
||||
|
||||
jest
|
||||
.mocked(PublishMutation.publishUpdateGroupAsync)
|
||||
.mockResolvedValue(platforms.map(platform => ({ ...updateStub, platform, runtimeVersion })));
|
||||
|
||||
await new UpdatePublish(flags, commandOptions).run();
|
||||
|
||||
// Pull the publish data from the mocked publish function
|
||||
const publishData = jest.mocked(PublishMutation.publishUpdateGroupAsync).mock.calls[0][1][0];
|
||||
// Pull the Expo config from the publish data
|
||||
const expoConfig = publishData.updateInfoGroup.ios!.extra.expoClient;
|
||||
|
||||
// Ensure the basic properties are present
|
||||
expect(expoConfig).toHaveProperty('name', appJson.expo.name);
|
||||
expect(expoConfig).toHaveProperty('slug', appJson.expo.slug);
|
||||
// Ensure non-public config is not present
|
||||
expect(expoConfig).not.toHaveProperty('hooks');
|
||||
});
|
||||
});
|
||||
|
||||
/** Create a new in-memory project, copied from src/commands/update/__tests__/republish.test.ts */
|
||||
function mockTestProject({
|
||||
configuredProjectId = '1234',
|
||||
expoConfig = {},
|
||||
}: {
|
||||
configuredProjectId?: string;
|
||||
} = {}): { projectId: string } {
|
||||
expoConfig?: Partial<ExpoConfig>;
|
||||
} = {}): { projectId: string; appJson: AppJSONConfig } {
|
||||
const packageJSON: PackageJSONConfig = {
|
||||
name: 'testing123',
|
||||
version: '0.1.0',
|
||||
@@ -157,6 +218,7 @@ function mockTestProject({
|
||||
projectId: configuredProjectId,
|
||||
},
|
||||
},
|
||||
...expoConfig,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -174,11 +236,27 @@ function mockTestProject({
|
||||
jest.mocked(getConfig).mockReturnValue(mockManifest as any);
|
||||
jest
|
||||
.spyOn(DynamicProjectConfigContextField.prototype, 'getValueAsync')
|
||||
.mockResolvedValue(async () => ({
|
||||
exp: mockManifest.exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
}));
|
||||
.mockResolvedValue(async (options?: GetConfigOptions) => {
|
||||
let exp = { ...mockManifest.exp };
|
||||
|
||||
// Fake a limited public config
|
||||
if (options?.isPublicConfig) {
|
||||
exp = {
|
||||
name: mockManifest.exp.name,
|
||||
version: mockManifest.exp.version,
|
||||
slug: mockManifest.exp.slug,
|
||||
sdkVersion: mockManifest.exp.sdkVersion,
|
||||
owner: mockManifest.exp.owner,
|
||||
extra: mockManifest.exp.extra,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
exp,
|
||||
projectDir: projectRoot,
|
||||
projectId: configuredProjectId,
|
||||
};
|
||||
});
|
||||
|
||||
jest.spyOn(LoggedInContextField.prototype, 'getValueAsync').mockResolvedValue({
|
||||
actor: jester,
|
||||
@@ -193,7 +271,7 @@ function mockTestProject({
|
||||
ownerAccount: jester.accounts[0],
|
||||
});
|
||||
|
||||
return { projectId: configuredProjectId };
|
||||
return { projectId: configuredProjectId, appJson: appJSON };
|
||||
}
|
||||
|
||||
/** Create a new in-memory export of the project */
|
||||
@@ -240,6 +318,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' }),
|
||||
manifestPermalink: 'https://expo.dev/fake/manifest/link',
|
||||
codeSigningInfo: null,
|
||||
createdAt: '2022-01-01T12:00:00Z',
|
||||
};
|
||||
|
||||
@@ -39,7 +39,11 @@ import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
|
||||
import { formatUpdateMessage, truncateString as truncateUpdateMessage } from '../../update/utils';
|
||||
import {
|
||||
formatUpdateMessage,
|
||||
getUpdateGroupJsonInfo,
|
||||
truncateString as truncateUpdateMessage,
|
||||
} from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
@@ -207,7 +211,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
projectId,
|
||||
});
|
||||
|
||||
const { exp } = await getDynamicProjectConfigAsync();
|
||||
const { exp } = await getDynamicProjectConfigAsync({ isPublicConfig: true });
|
||||
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
|
||||
|
||||
let realizedPlatforms: PublishPlatform[] = [];
|
||||
@@ -314,10 +318,34 @@ export default class UpdatePublish extends EasCommand {
|
||||
uploadedAssetCount = uploadResults.uniqueUploadedAssetCount;
|
||||
assetLimitPerUpdateGroup = uploadResults.assetLimitPerUpdateGroup;
|
||||
unsortedUpdateInfoGroups = await buildUnsortedUpdateInfoGroupAsync(assets, exp);
|
||||
const uploadAssetSuccessMessage = uploadedAssetCount
|
||||
? `Uploaded ${uploadedAssetCount} ${uploadedAssetCount === 1 ? 'platform' : 'platforms'}`
|
||||
: `Uploaded: No changes detected`;
|
||||
assetSpinner.succeed(uploadAssetSuccessMessage);
|
||||
|
||||
// NOTE(cedric): we assume that bundles are always uploaded, and always are part of
|
||||
// `uploadedAssetCount`, perferably we don't assume. For that, we need to refactor the
|
||||
// `uploadAssetsAsync` and be able to determine asset type from the uploaded assets.
|
||||
const uploadedBundleCount = uploadResults.launchAssetCount;
|
||||
const uploadedNormalAssetCount = Math.max(0, uploadedAssetCount - uploadedBundleCount);
|
||||
const reusedNormalAssetCount = uploadResults.uniqueAssetCount - uploadedNormalAssetCount;
|
||||
|
||||
assetSpinner.stop();
|
||||
Log.withTick(
|
||||
`Uploaded ${uploadedBundleCount} app ${uploadedBundleCount === 1 ? 'bundle' : 'bundles'}`
|
||||
);
|
||||
if (uploadedNormalAssetCount === 0) {
|
||||
Log.withTick(`Uploading assets skipped - no new assets found`);
|
||||
} else {
|
||||
let message = `Uploaded ${uploadedNormalAssetCount} ${
|
||||
uploadedNormalAssetCount === 1 ? 'asset' : 'assets'
|
||||
}`;
|
||||
if (reusedNormalAssetCount > 0) {
|
||||
message += ` (reused ${reusedNormalAssetCount} ${
|
||||
reusedNormalAssetCount === 1 ? 'asset' : 'assets'
|
||||
})`;
|
||||
}
|
||||
Log.withTick(message);
|
||||
}
|
||||
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
|
||||
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
|
||||
}
|
||||
} catch (e) {
|
||||
assetSpinner.fail('Failed to upload');
|
||||
throw e;
|
||||
@@ -434,7 +462,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
}
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(newUpdates);
|
||||
printJsonOnlyOutput(getUpdateGroupJsonInfo(newUpdates));
|
||||
} else {
|
||||
if (new Set(newUpdates.map(update => update.group)).size > 1) {
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
@@ -4,7 +4,11 @@ import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
|
||||
import Log from '../../log';
|
||||
import { formatUpdateGroup, getUpdateGroupDescriptions } from '../../update/utils';
|
||||
import {
|
||||
formatUpdateGroup,
|
||||
getUpdateGroupDescriptions,
|
||||
getUpdateGroupJsonInfo,
|
||||
} from '../../update/utils';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class UpdateView extends EasCommand {
|
||||
@@ -41,11 +45,12 @@ export default class UpdateView extends EasCommand {
|
||||
}
|
||||
|
||||
const updatesByGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, { groupId });
|
||||
const [updateGroupDescription] = getUpdateGroupDescriptions([updatesByGroup]);
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(updateGroupDescription);
|
||||
printJsonOnlyOutput(getUpdateGroupJsonInfo(updatesByGroup));
|
||||
} else {
|
||||
const [updateGroupDescription] = getUpdateGroupDescriptions([updatesByGroup]);
|
||||
|
||||
Log.log(chalk.bold('Update group:'));
|
||||
|
||||
Log.log(formatUpdateGroup(updateGroupDescription));
|
||||
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
|
||||
+564
-135
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: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -8,8 +9,10 @@ import {
|
||||
GetSignedUploadMutationVariables,
|
||||
PublishUpdateGroupInput,
|
||||
SetCodeSigningInfoMutation,
|
||||
UpdateFragment,
|
||||
UpdatePublishMutation,
|
||||
} from '../generated';
|
||||
import { UpdateFragmentNode } from '../types/Update';
|
||||
|
||||
export const PublishMutation = {
|
||||
async getUploadURLsAsync(
|
||||
@@ -39,7 +42,7 @@ export const PublishMutation = {
|
||||
async publishUpdateGroupAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
publishUpdateGroupsInput: PublishUpdateGroupInput[]
|
||||
): Promise<UpdatePublishMutation['updateBranch']['publishUpdateGroups']> {
|
||||
): Promise<UpdateFragment[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<UpdatePublishMutation>(
|
||||
@@ -48,13 +51,11 @@ export const PublishMutation = {
|
||||
updateBranch {
|
||||
publishUpdateGroups(publishUpdateGroupsInput: $publishUpdateGroupsInput) {
|
||||
id
|
||||
group
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestPermalink
|
||||
...UpdateFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(UpdateFragmentNode)}
|
||||
`,
|
||||
{ publishUpdateGroupsInput }
|
||||
)
|
||||
|
||||
@@ -26,6 +26,9 @@ export const UserQuery = {
|
||||
... on Robot {
|
||||
firstName
|
||||
}
|
||||
... on SSOUser {
|
||||
username
|
||||
}
|
||||
accounts {
|
||||
id
|
||||
...AccountFragment
|
||||
|
||||
@@ -52,7 +52,9 @@ export const BuildFragmentNode = gql`
|
||||
priority
|
||||
createdAt
|
||||
updatedAt
|
||||
message
|
||||
completedAt
|
||||
resourceClass
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ export const UpdateFragmentNode = gql`
|
||||
runtimeVersion
|
||||
platform
|
||||
manifestFragment
|
||||
manifestPermalink
|
||||
gitCommitHash
|
||||
actor {
|
||||
__typename
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -16,7 +16,9 @@ import {
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
filterExportedPlatformsByFlag,
|
||||
filterOutAssetsThatAlreadyExistAsync,
|
||||
getAssetHashFromPath,
|
||||
getBase64URLEncoding,
|
||||
getOriginalPathFromAssetMap,
|
||||
getStorageKey,
|
||||
getStorageKeyForAssetAsync,
|
||||
guessContentTypeFromExtension,
|
||||
@@ -279,9 +281,55 @@ describe(resolveInputDirectoryAsync, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(getAssetHashFromPath, () => {
|
||||
it('returns asset hash from path', () => {
|
||||
expect(getAssetHashFromPath('assets/5b2a819c71d035ca45d223e4c47ed4f9')).toBe(
|
||||
'5b2a819c71d035ca45d223e4c47ed4f9'
|
||||
);
|
||||
});
|
||||
it('returns null for incorrect path', () => {
|
||||
expect(getAssetHashFromPath('assets/this/is/not/a/hash.jpg')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe(getOriginalPathFromAssetMap, () => {
|
||||
// Partial assetmap.json, with only fields we need
|
||||
const fakeAssetMap = {
|
||||
'5b2a819c71d035ca45d223e4c47ed4f9': {
|
||||
httpServerLocation: '/assets/src/assets',
|
||||
name: 'asset-420',
|
||||
type: 'jpg',
|
||||
},
|
||||
};
|
||||
it('returns null path when asset map is null', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(null, {
|
||||
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
it('returns null when asset is not found in asset map', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(fakeAssetMap, {
|
||||
path: 'assets/fb64d3b2fb71b3d739ad5c13a93e12c5',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBeNull();
|
||||
});
|
||||
it('returns reconstructed original path from existing asset in asset map', () => {
|
||||
expect(
|
||||
getOriginalPathFromAssetMap(fakeAssetMap, {
|
||||
path: 'assets/5b2a819c71d035ca45d223e4c47ed4f9',
|
||||
ext: 'jpg',
|
||||
})
|
||||
).toBe('/src/assets/asset-420.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe(collectAssetsAsync, () => {
|
||||
it('builds an update info group', async () => {
|
||||
const fakeHash = 'md5-hash-of-jpg';
|
||||
const fakeHash = 'hdahukw8adhawi8fawhfa8';
|
||||
const bundles = {
|
||||
android: 'android-bundle-code',
|
||||
ios: 'ios-bundle-code',
|
||||
@@ -294,6 +342,7 @@ describe(collectAssetsAsync, () => {
|
||||
fileExtension: '.jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: `${inputDir}/assets/${fakeHash}`,
|
||||
originalPath: `assets/wat.jpg`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -326,6 +375,24 @@ describe(collectAssetsAsync, () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.resolve(inputDir, 'assetmap.json'),
|
||||
JSON.stringify({
|
||||
[fakeHash]: {
|
||||
__packager_asset: true,
|
||||
fileSystemLocation: '/Users/blah/temp/assets',
|
||||
httpServerLocation: 'assets/assets',
|
||||
width: 2339,
|
||||
height: 1560,
|
||||
scales: [1],
|
||||
files: ['/Users/blah/temp/assets/wat.jpg'],
|
||||
hash: fakeHash,
|
||||
name: 'wat',
|
||||
type: 'jpg',
|
||||
fileHashes: [fakeHash],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(await collectAssetsAsync(inputDir)).toEqual({
|
||||
android: {
|
||||
@@ -410,6 +477,7 @@ describe(uploadAssetsAsync, () => {
|
||||
const androidBundlePath = uuidv4();
|
||||
const iosBundlePath = uuidv4();
|
||||
const dummyFilePath = uuidv4();
|
||||
const dummyOriginalFilePath = uuidv4();
|
||||
const userDefinedPath = uuidv4();
|
||||
const testProjectId = uuidv4();
|
||||
const expectedAssetLimit = 1400;
|
||||
@@ -427,7 +495,15 @@ describe(uploadAssetsAsync, () => {
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
ios: {
|
||||
launchAsset: {
|
||||
@@ -435,7 +511,15 @@ describe(uploadAssetsAsync, () => {
|
||||
contentType: 'application/javascript',
|
||||
path: androidBundlePath,
|
||||
},
|
||||
assets: [userDefinedAsset, { type: 'jpg', contentType: 'image/jpeg', path: dummyFilePath }],
|
||||
assets: [
|
||||
userDefinedAsset,
|
||||
{
|
||||
type: 'jpg',
|
||||
contentType: 'image/jpeg',
|
||||
path: dummyFilePath,
|
||||
originalPath: dummyOriginalFilePath,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -490,8 +574,10 @@ describe(uploadAssetsAsync, () => {
|
||||
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
|
||||
).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
launchAssetCount: 2,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 0,
|
||||
uniqueUploadedAssetPaths: [],
|
||||
assetLimitPerUpdateGroup: expectedAssetLimit,
|
||||
});
|
||||
});
|
||||
@@ -526,8 +612,10 @@ describe(uploadAssetsAsync, () => {
|
||||
uploadAssetsAsync(graphqlClient, assetsForUpdateInfoGroup, testProjectId)
|
||||
).resolves.toEqual({
|
||||
assetCount: 6,
|
||||
launchAssetCount: 2,
|
||||
uniqueAssetCount: 3,
|
||||
uniqueUploadedAssetCount: 2,
|
||||
uniqueUploadedAssetPaths: [],
|
||||
assetLimitPerUpdateGroup: expectedAssetLimit,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,26 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
export async function validateCustomBuildConfigAsync(
|
||||
projectDir: string,
|
||||
profile: BuildProfile<Platform>
|
||||
): Promise<void> {
|
||||
if (!profile.config) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeBuildConfigPath = getCustomBuildConfigPath(profile.config);
|
||||
const buildConfigPath = path.join(projectDir, relativeBuildConfigPath);
|
||||
if (!(await fs.pathExists(buildConfigPath))) {
|
||||
throw new Error(
|
||||
`Custom build configuration file ${chalk.bold(relativeBuildConfigPath)} does not exist.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomBuildConfigPath(configFilename: string): string {
|
||||
return path.join('.eas/build', configFilename);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -25,6 +25,8 @@ export function getUsername(exp: ExpoConfig, user: Actor): string | undefined {
|
||||
}
|
||||
// robot users don't have usernames
|
||||
return undefined;
|
||||
case 'SSOUser':
|
||||
throw new Error('SSO users are not supported yet.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import Log from '../log';
|
||||
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import { expoCommandAsync, shouldUseVersionedExpoCLI } from '../utils/expoCli';
|
||||
import chunk from '../utils/expodash/chunk';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
|
||||
export type ExpoCLIExportPlatformFlag = Platform | 'all';
|
||||
@@ -30,6 +31,8 @@ export type RawAsset = {
|
||||
fileExtension?: string;
|
||||
contentType: string;
|
||||
path: string;
|
||||
/** Original asset path derrived from asset map, or exported folder */
|
||||
originalPath?: string;
|
||||
};
|
||||
|
||||
type CollectedAssets = {
|
||||
@@ -52,6 +55,16 @@ type UpdateInfoGroup = {
|
||||
[key in Platform]: ManifestFragment;
|
||||
};
|
||||
|
||||
// Partial copy of `@expo/dev-server` `BundleAssetWithFileHashes`
|
||||
type AssetMap = Record<
|
||||
string,
|
||||
{
|
||||
httpServerLocation: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
>;
|
||||
|
||||
const fileMetadataJoi = Joi.object({
|
||||
assets: Joi.array()
|
||||
.required()
|
||||
@@ -170,6 +183,7 @@ export async function buildBundlesAsync({
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
'--dump-assetmap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
@@ -182,6 +196,7 @@ export async function buildBundlesAsync({
|
||||
'--experimental-bundle',
|
||||
'--non-interactive',
|
||||
'--dump-sourcemap',
|
||||
'--dump-assetmap',
|
||||
'--platform',
|
||||
platformFlag,
|
||||
]);
|
||||
@@ -246,9 +261,48 @@ export function filterExportedPlatformsByFlag<T extends Partial<Record<Platform,
|
||||
return { [platform]: record[platform] } as T;
|
||||
}
|
||||
|
||||
/** Try to load the asset map for logging the names of assets published */
|
||||
export async function loadAssetMapAsync(distRoot: string): Promise<AssetMap | null> {
|
||||
const assetMapPath = path.join(distRoot, 'assetmap.json');
|
||||
|
||||
if (!(await fs.pathExists(assetMapPath))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const assetMap: AssetMap = JsonFile.read(path.join(distRoot, 'assetmap.json'));
|
||||
// TODO: basic validation?
|
||||
return assetMap;
|
||||
}
|
||||
|
||||
// exposed for testing
|
||||
export function getAssetHashFromPath(assetPath: string): string | null {
|
||||
const [, hash] = assetPath.match(new RegExp(/assets\/([a-z0-9]+)$/, 'i')) ?? [];
|
||||
return hash ?? null;
|
||||
}
|
||||
|
||||
// exposed for testing
|
||||
export function getOriginalPathFromAssetMap(
|
||||
assetMap: AssetMap | null,
|
||||
asset: { path: string; ext: string }
|
||||
): string | null {
|
||||
if (!assetMap) {
|
||||
return null;
|
||||
}
|
||||
const assetHash = getAssetHashFromPath(asset.path);
|
||||
const assetMapEntry = assetHash && assetMap[assetHash];
|
||||
|
||||
if (!assetMapEntry) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathPrefix = assetMapEntry.httpServerLocation.substring('/assets'.length);
|
||||
return `${pathPrefix}/${assetMapEntry.name}.${assetMapEntry.type}`;
|
||||
}
|
||||
|
||||
/** Given a directory, load the metadata.json and collect the assets for each platform. */
|
||||
export async function collectAssetsAsync(dir: string): Promise<CollectedAssets> {
|
||||
const metadata = loadMetadata(dir);
|
||||
const assetmap = await loadAssetMapAsync(dir);
|
||||
|
||||
const collectedAssets: CollectedAssets = {};
|
||||
|
||||
@@ -261,6 +315,7 @@ export async function collectAssetsAsync(dir: string): Promise<CollectedAssets>
|
||||
},
|
||||
assets: metadata.fileMetadata[platform].assets.map(asset => ({
|
||||
fileExtension: asset.ext ? ensureLeadingPeriod(asset.ext) : undefined,
|
||||
originalPath: getOriginalPathFromAssetMap(assetmap, asset) ?? undefined,
|
||||
contentType: guessContentTypeFromExtension(asset.ext),
|
||||
path: path.join(dir, asset.path),
|
||||
})),
|
||||
@@ -294,9 +349,17 @@ export async function filterOutAssetsThatAlreadyExistAsync(
|
||||
}
|
||||
|
||||
type AssetUploadResult = {
|
||||
/** All found assets within the exported folder per platform */
|
||||
assetCount: number;
|
||||
/** The uploaded JS bundles, per platform */
|
||||
launchAssetCount: number;
|
||||
/** All unique assets within the exported folder with platforms combined */
|
||||
uniqueAssetCount: number;
|
||||
/** All unique assets uploaded */
|
||||
uniqueUploadedAssetCount: number;
|
||||
/** All (non-launch) asset original paths, used for logging */
|
||||
uniqueUploadedAssetPaths: string[];
|
||||
/** The asset limit received from the server */
|
||||
assetLimitPerUpdateGroup: number;
|
||||
};
|
||||
|
||||
@@ -308,7 +371,9 @@ export async function uploadAssetsAsync(
|
||||
): Promise<AssetUploadResult> {
|
||||
let assets: RawAsset[] = [];
|
||||
let platform: keyof CollectedAssets;
|
||||
const launchAssets: RawAsset[] = [];
|
||||
for (platform in assetsForUpdateInfoGroup) {
|
||||
launchAssets.push(assetsForUpdateInfoGroup[platform]!.launchAsset);
|
||||
assets = [
|
||||
...assets,
|
||||
assetsForUpdateInfoGroup[platform]!.launchAsset,
|
||||
@@ -335,6 +400,7 @@ export async function uploadAssetsAsync(
|
||||
updateSpinnerText?.(totalAssets, totalAssets);
|
||||
let missingAssets = await filterOutAssetsThatAlreadyExistAsync(graphqlClient, uniqueAssets);
|
||||
const uniqueUploadedAssetCount = missingAssets.length;
|
||||
const uniqueUploadedAssetPaths = missingAssets.map(asset => asset.originalPath).filter(truthy);
|
||||
|
||||
const missingAssetChunks = chunk(missingAssets, 100);
|
||||
const specifications: string[] = [];
|
||||
@@ -372,8 +438,10 @@ export async function uploadAssetsAsync(
|
||||
}
|
||||
return {
|
||||
assetCount: assets.length,
|
||||
launchAssetCount: launchAssets.length,
|
||||
uniqueAssetCount: uniqueAssets.length,
|
||||
uniqueUploadedAssetCount,
|
||||
uniqueUploadedAssetPaths,
|
||||
assetLimitPerUpdateGroup,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,6 +121,7 @@ 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: '...',
|
||||
activityTimestamp: '2022-12-07T02:24:43.487Z',
|
||||
|
||||
@@ -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']> {
|
||||
|
||||
@@ -24,6 +24,18 @@ export type FormatUpdateParameter = Pick<Update, 'id' | 'createdAt' | 'message'>
|
||||
actor?: Maybe<Pick<User, 'username' | 'id'> | Pick<Robot, 'firstName' | 'id'>>;
|
||||
};
|
||||
|
||||
export type UpdateJsonInfo = { branch: string } & Pick<
|
||||
UpdateFragment,
|
||||
| 'id'
|
||||
| 'createdAt'
|
||||
| 'group'
|
||||
| 'message'
|
||||
| 'runtimeVersion'
|
||||
| 'platform'
|
||||
| 'manifestPermalink'
|
||||
| 'gitCommitHash'
|
||||
>;
|
||||
|
||||
export type UpdateGroupDescription = FormatUpdateParameter & {
|
||||
branch: string;
|
||||
group: string;
|
||||
@@ -181,6 +193,20 @@ export function formatUpdateTitle(update: UpdateFragment): string {
|
||||
)} by ${actorName}, runtimeVersion: ${runtimeVersion}] ${message}`;
|
||||
}
|
||||
|
||||
export function getUpdateGroupJsonInfo(updateGroups: UpdateFragment[]): UpdateJsonInfo[] {
|
||||
return updateGroups.map(update => ({
|
||||
id: update.id,
|
||||
createdAt: update.createdAt,
|
||||
group: update.group,
|
||||
branch: update.branch.name,
|
||||
message: update.message,
|
||||
runtimeVersion: update.runtimeVersion,
|
||||
platform: update.platform,
|
||||
manifestPermalink: update.manifestPermalink,
|
||||
gitCommitHash: update.gitCommitHash,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getUpdateGroupDescriptions(
|
||||
updateGroups: UpdateFragment[][]
|
||||
): FormattedUpdateGroupDescription[] {
|
||||
|
||||
@@ -7,13 +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 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-----
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
convertCertificatePEMToCertificate,
|
||||
convertPrivateKeyPEMToPrivateKey,
|
||||
signStringRSASHA256AndVerify,
|
||||
signBufferRSASHA256AndVerify,
|
||||
validateSelfSignedCertificate,
|
||||
} from '@expo/code-signing-certificates';
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
@@ -116,10 +116,10 @@ export async function getManifestBodyAsync(res: Response): Promise<string | null
|
||||
}
|
||||
|
||||
export function signManifestBody(body: string, codeSigningInfo: CodeSigningInfo): string {
|
||||
return signStringRSASHA256AndVerify(
|
||||
return signBufferRSASHA256AndVerify(
|
||||
codeSigningInfo.privateKey,
|
||||
codeSigningInfo.certificate,
|
||||
body
|
||||
Buffer.from(body, 'utf-8')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import log from '../log';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import GitNoCommitClient from './clients/gitNoCommit';
|
||||
import NoVcsClient from './clients/noVcs';
|
||||
import { Client } from './vcs';
|
||||
@@ -10,7 +11,11 @@ let vcsClient = resolveDefaultVcsClient();
|
||||
function resolveDefaultVcsClient(): Client {
|
||||
if (process.env.EAS_NO_VCS) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
log.warn(NO_VCS_WARNING);
|
||||
// This log might be printed before cli arguments are evaluated,
|
||||
// so it needs to go to stderr in case command is run in JSON
|
||||
// only mode.
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(chalk.yellow(NO_VCS_WARNING));
|
||||
}
|
||||
return new NoVcsClient();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export class Ignore {
|
||||
if (await fs.pathExists(easIgnorePath)) {
|
||||
this.ignoreMapping = [
|
||||
['', createIgnore().add(DEFAULT_IGNORE)],
|
||||
['', createIgnore().add(await this.readIgnoreFileAsync(easIgnorePath))],
|
||||
['', createIgnore().add(await fs.readFile(easIgnorePath, 'utf-8'))],
|
||||
];
|
||||
return;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ export class Ignore {
|
||||
ignoreFilePaths.map(async filePath => {
|
||||
return [
|
||||
filePath.slice(0, filePath.length - GITIGNORE_FILENAME.length),
|
||||
createIgnore().add(await this.readIgnoreFileAsync(path.join(this.rootDir, filePath))),
|
||||
createIgnore().add(await fs.readFile(path.join(this.rootDir, filePath), 'utf-8')),
|
||||
] as const;
|
||||
})
|
||||
);
|
||||
@@ -72,16 +72,6 @@ export class Ignore {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async readIgnoreFileAsync(filePath: string): Promise<string> {
|
||||
const fileContents = await fs.readFile(filePath, 'utf-8');
|
||||
const lines = fileContents.split('\n');
|
||||
// Strip trailing '\'. This logic can be removed after fix upstream is released.
|
||||
// https://github.com/kaelzhang/node-ignore/issues/81
|
||||
return lines
|
||||
.map((line: string) => (line.slice(-1) === '\\' ? line.slice(0, -1) : line))
|
||||
.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeShallowCopyAsync(src: string, dst: string): Promise<void> {
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "3.0.0",
|
||||
"version": "3.8.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.18.6",
|
||||
"@expo/eas-build-job": "0.2.97",
|
||||
"@expo/eas-build-job": "0.2.106",
|
||||
"chalk": "4.1.2",
|
||||
"env-string": "1.0.1",
|
||||
"fs-extra": "10.1.0",
|
||||
"golden-fleece": "1.0.9",
|
||||
"joi": "17.6.4",
|
||||
"joi": "17.7.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"semver": "7.3.8",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.0"
|
||||
"tslib": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/babel__code-frame": "7.0.3",
|
||||
"@types/fs-extra": "9.0.13",
|
||||
"memfs": "3.4.7",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "4.8.4"
|
||||
"typescript": "4.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
@@ -36,7 +36,8 @@
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"prepack": "rimraf build && yarn build",
|
||||
"prepack": "yarn rebuild",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"test": "jest",
|
||||
"clean": "rimraf build node_modules yarn-error.log"
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"When using remote, the `autoIncrement` is based on the values stored on EAS servers."
|
||||
]
|
||||
},
|
||||
"promptToConfigurePushNotifications" : {
|
||||
"promptToConfigurePushNotifications": {
|
||||
"type": "boolean",
|
||||
"description": "Specifies where EAS CLI should prompt to configure Push Notifications credentials. Defaults to true.",
|
||||
"default": true
|
||||
@@ -176,6 +176,19 @@
|
||||
"description": "List of paths (or patterns) where EAS Build is going to look for the build artifacts. Use `applicationArchivePath` to specify the path for uploading the application archive. Build artifacts are uploaded even if the build fails. See: https://github.com/mrmlnc/fast-glob#pattern-syntax",
|
||||
"markdownDescription": "List of paths (or patterns) where EAS Build is going to look for the build artifacts. Use `applicationArchivePath` to specify the path for uploading the application archive. Build artifacts are uploaded even if the build fails. EAS Build uses the `fast-glob` npm package for pattern matching ([see their README to learn more about the syntax you can use](https://github.com/mrmlnc/fast-glob#pattern-syntax))."
|
||||
},
|
||||
"resourceClass": {
|
||||
"description": "The resource class that will be used to run this build, supported by both Android and iOS workers. Learn more: https://docs.expo.dev/build-reference/infrastructure/",
|
||||
"markdownDescription": "The resource class that will be used to run this build, supported by both Android and iOS workers. [Learn more](https://docs.expo.dev/build-reference/infrastructure/)",
|
||||
"default": "default",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["default", "medium"]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
"$ref": "#/definitions/BuildProfileAndroid"
|
||||
},
|
||||
@@ -222,6 +235,19 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceClass": {
|
||||
"description": "The Android-specific resource class that will be used to run this build. Learn more: https://docs.expo.dev/build-reference/infrastructure/",
|
||||
"markdownDescription": "The Android-specific resource class that will be used to run this build. [Learn more](https://docs.expo.dev/build-reference/infrastructure/)",
|
||||
"default": "default",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["default", "medium", "large"]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ndk": {
|
||||
"type": "string",
|
||||
"description": "The version of Android NDK."
|
||||
@@ -303,6 +329,7 @@
|
||||
"enum": [
|
||||
"default",
|
||||
"latest",
|
||||
"macos-monterey-12.6-xcode-14.2",
|
||||
"macos-monterey-12.6-xcode-14.1",
|
||||
"macos-monterey-12.6-xcode-14.0",
|
||||
"macos-monterey-12.4-xcode-13.4",
|
||||
@@ -311,7 +338,23 @@
|
||||
"macos-big-sur-11.4-xcode-13.0",
|
||||
"macos-big-sur-11.4-xcode-12.5"
|
||||
],
|
||||
"markdownEnumDescriptions": ["`macos-monterey-12.6-xcode-14.0`", "`macos-monterey-12.6-xcode-14.1`"]
|
||||
"markdownEnumDescriptions": [
|
||||
"`macos-monterey-12.6-xcode-14.1`",
|
||||
"`macos-monterey-12.6-xcode-14.2`"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceClass": {
|
||||
"description": "The iOS-specific resource class that will be used to run this build. Learn more: https://docs.expo.dev/build-reference/infrastructure/",
|
||||
"markdownDescription": "The iOS-specific resource class that will be used to run this build. [Learn more](https://docs.expo.dev/build-reference/infrastructure/)",
|
||||
"default": "default",
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": ["default", "medium", "intel-medium", "m-medium"]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
|
||||
@@ -349,3 +349,114 @@ test('get profile names', async () => {
|
||||
const allProfileNames = await EasJsonUtils.getBuildProfileNamesAsync(accessor);
|
||||
expect(allProfileNames.sort()).toEqual(['blah', 'production'].sort());
|
||||
});
|
||||
|
||||
test('invalid resourceClass at build profile root', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
resourceClass: 'm1-experimental',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).rejects.toThrowError(/build.production.resourceClass.*must be one of/);
|
||||
});
|
||||
|
||||
test('iOS-specific resourceClass', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
ios: {
|
||||
resourceClass: 'm1-medium',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production')
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('Android-specific resourceClass', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
android: {
|
||||
resourceClass: 'large',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
await expect(
|
||||
EasJsonUtils.getBuildProfileAsync(accessor, Platform.ANDROID, 'production')
|
||||
).resolves.not.toThrow();
|
||||
});
|
||||
|
||||
test('build profile with platform-specific custom build config', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
android: {
|
||||
config: 'production.android.yml',
|
||||
},
|
||||
ios: {
|
||||
config: 'production.ios.yml',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
expect(androidProfile).toEqual({
|
||||
config: 'production.android.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
expect(iosProfile).toEqual({
|
||||
config: 'production.ios.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
|
||||
test('build profiles with both platform build config', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
production: {
|
||||
config: 'production.yml',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = new EasJsonAccessor('/project');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'production'
|
||||
);
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'production');
|
||||
expect(androidProfile).toEqual({
|
||||
config: 'production.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
expect(iosProfile).toEqual({
|
||||
config: 'production.yml',
|
||||
distribution: 'store',
|
||||
credentialsSource: 'remote',
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,23 @@
|
||||
import { Android, Ios } from '@expo/eas-build-job';
|
||||
import Joi from 'joi';
|
||||
import semver from 'semver';
|
||||
|
||||
import { ResourceClass } from './types';
|
||||
|
||||
const AllowedCommonResourceClasses: ResourceClass[] = [ResourceClass.DEFAULT, ResourceClass.MEDIUM];
|
||||
|
||||
const AllowedAndroidResourceClasses: ResourceClass[] = [
|
||||
...AllowedCommonResourceClasses,
|
||||
ResourceClass.LARGE,
|
||||
];
|
||||
|
||||
const AllowedIosResourceClasses: ResourceClass[] = [
|
||||
...AllowedCommonResourceClasses,
|
||||
ResourceClass.M1_MEDIUM,
|
||||
ResourceClass.INTEL_MEDIUM,
|
||||
ResourceClass.M_MEDIUM,
|
||||
ResourceClass.M_LARGE,
|
||||
];
|
||||
|
||||
const CacheSchema = Joi.object({
|
||||
disabled: Joi.boolean(),
|
||||
key: Joi.string().max(128),
|
||||
@@ -10,66 +26,98 @@ const CacheSchema = Joi.object({
|
||||
});
|
||||
|
||||
const CommonBuildProfileSchema = Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
cache: CacheSchema,
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
developmentClient: Joi.boolean(),
|
||||
prebuildCommand: Joi.string(),
|
||||
buildArtifactPaths: Joi.array().items(Joi.string()),
|
||||
// builder
|
||||
resourceClass: Joi.string().valid(...AllowedCommonResourceClasses),
|
||||
|
||||
// build environment
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
node: Joi.string().empty(null).custom(semverCheck),
|
||||
yarn: Joi.string().empty(null).custom(semverCheck),
|
||||
expoCli: Joi.string().empty(null).custom(semverCheck),
|
||||
env: Joi.object().pattern(Joi.string(), Joi.string().empty(null)),
|
||||
|
||||
// credentials
|
||||
credentialsSource: Joi.string().valid('local', 'remote').default('remote'),
|
||||
distribution: Joi.string().valid('store', 'internal').default('store'),
|
||||
|
||||
// updates
|
||||
releaseChannel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
channel: Joi.string().regex(/^[a-z\d][a-z\d._-]*$/),
|
||||
|
||||
// build configuration
|
||||
developmentClient: Joi.boolean(),
|
||||
prebuildCommand: Joi.string(),
|
||||
|
||||
// versions
|
||||
autoIncrement: Joi.alternatives().try(Joi.boolean()),
|
||||
|
||||
// artifacts
|
||||
buildArtifactPaths: Joi.array().items(Joi.string()),
|
||||
|
||||
// cache
|
||||
cache: CacheSchema,
|
||||
|
||||
// custom build configuration
|
||||
config: Joi.string(),
|
||||
});
|
||||
|
||||
const AndroidBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
// build environment
|
||||
image: Joi.string(),
|
||||
|
||||
// artifacts
|
||||
artifactPath: Joi.string(),
|
||||
applicationArchivePath: Joi.string(),
|
||||
}).oxor('artifactPath', 'applicationArchivePath')
|
||||
);
|
||||
|
||||
const AndroidBuildProfileSchema = PlatformBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
// builder
|
||||
resourceClass: Joi.string().valid(...AllowedAndroidResourceClasses),
|
||||
|
||||
// build environment
|
||||
ndk: Joi.string().empty(null).custom(semverCheck),
|
||||
|
||||
// credentials
|
||||
withoutCredentials: Joi.boolean(),
|
||||
|
||||
image: Joi.string().valid(...Android.builderBaseImages),
|
||||
ndk: Joi.string().empty(null).custom(semverCheck),
|
||||
// build configuration
|
||||
gradleCommand: Joi.string(),
|
||||
buildType: Joi.string().valid('apk', 'app-bundle'),
|
||||
|
||||
// versions
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'versionCode')
|
||||
),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
applicationArchivePath: Joi.string(),
|
||||
buildArtifactPaths: Joi.array().items(Joi.string()),
|
||||
gradleCommand: Joi.string(),
|
||||
|
||||
buildType: Joi.string().valid('apk', 'app-bundle'),
|
||||
}).oxor('artifactPath', 'applicationArchivePath')
|
||||
})
|
||||
);
|
||||
|
||||
const IosBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
const IosBuildProfileSchema = PlatformBuildProfileSchema.concat(
|
||||
Joi.object({
|
||||
credentialsSource: Joi.string().valid('local', 'remote'),
|
||||
distribution: Joi.string().valid('store', 'internal'),
|
||||
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'buildNumber')
|
||||
),
|
||||
simulator: Joi.boolean(),
|
||||
// builder
|
||||
resourceClass: Joi.string().valid(...AllowedIosResourceClasses),
|
||||
|
||||
image: Joi.string().valid(...Ios.builderBaseImages),
|
||||
// build environment
|
||||
bundler: Joi.string().empty(null).custom(semverCheck),
|
||||
fastlane: Joi.string().empty(null).custom(semverCheck),
|
||||
cocoapods: Joi.string().empty(null).custom(semverCheck),
|
||||
|
||||
artifactPath: Joi.string(),
|
||||
applicationArchivePath: Joi.string(),
|
||||
buildArtifactPaths: Joi.array().items(Joi.string()),
|
||||
// credentials
|
||||
enterpriseProvisioning: Joi.string().valid('adhoc', 'universal'),
|
||||
|
||||
// build configuration
|
||||
simulator: Joi.boolean(),
|
||||
scheme: Joi.string(),
|
||||
buildConfiguration: Joi.string(),
|
||||
}).oxor('artifactPath', 'applicationArchivePath')
|
||||
|
||||
// versions
|
||||
autoIncrement: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('version', 'buildNumber')
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
export const BuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
|
||||
Loaded 100 of 105 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user