mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
1
Commits
@quin/qr-code
..
wer
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aadef09d1e |
No files matched your search
@@ -1,5 +1,2 @@
|
|||||||
* text eol=lf
|
* text eol=lf
|
||||||
*.png binary
|
|
||||||
*.jpg binary
|
|
||||||
|
|
||||||
**/yarn.lock linguist-generated
|
**/yarn.lock linguist-generated
|
||||||
@@ -37,7 +37,7 @@ body:
|
|||||||
required: true
|
required: true
|
||||||
attributes:
|
attributes:
|
||||||
label: Environment
|
label: Environment
|
||||||
description: Run `npx expo-env-info` and `npx expo-doctor`, and paste the output here.
|
description: Run `npx expo-env-info` and `expo doctor`, and paste the output here.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
contact_links:
|
|
||||||
- name: Expo Developers Discord
|
|
||||||
url: https://chat.expo.dev/
|
|
||||||
about: Join other developers in discussions regarding Expo and EAS.
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
<!-- If this PR requires a changelog entry, add it by commenting the PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`. -->
|
<!-- Thanks for contributing to _EAS CLI_! Before you submit your pull request, please make sure to check the following boxes by putting an x in the [ ] (don't: [x ], [ x], do: [x]) -->
|
||||||
<!-- You can skip the changelog check by labeling the PR with "no changelog". -->
|
|
||||||
|
# Checklist
|
||||||
|
|
||||||
|
- [ ] I've added an entry to [CHANGELOG.md](https://github.com/expo/eas-cli/blob/main/CHANGELOG.md#main) if necessary. You can comment this pull request with `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]` and CHANGELOG.md will be updated automatically.
|
||||||
|
|
||||||
# Why
|
# Why
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"singleQuote": true
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
name: notify-slack-on-fail-or-recover
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
channel:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
slack_webhook:
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: 'composite'
|
|
||||||
steps:
|
|
||||||
- name: Get previous workflow run status
|
|
||||||
uses: actions/github-script@v6
|
|
||||||
id: run-status
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const script = require('.github/internal-actions/notify-slack-on-fail-or-recover/main.js');
|
|
||||||
return await script({github, context, core});
|
|
||||||
|
|
||||||
- name: Commit subject
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
subject="$(git show --format=%s --quiet | sed 's/"/\\"/g')"
|
|
||||||
echo "${{ fromJSON(steps.run-status.outputs.result).previous }}"
|
|
||||||
echo "SUBJECT=$subject" >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Send Slack Success Notification
|
|
||||||
if: fromJSON(steps.run-status.outputs.result).previous == 'failure' && fromJSON(steps.run-status.outputs.result).current == 'success'
|
|
||||||
uses: rtCamp/action-slack-notify@v2
|
|
||||||
env:
|
|
||||||
SLACK_CHANNEL: ${{ inputs.channel }}
|
|
||||||
SLACK_COLOR: good
|
|
||||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
|
||||||
SLACK_TITLE: 'Fixed, was failing'
|
|
||||||
SLACK_MESSAGE: ${{ env.SUBJECT }}
|
|
||||||
SLACK_USERNAME: 'EAS CLI CI'
|
|
||||||
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
|
|
||||||
SLACK_FOOTER: ''
|
|
||||||
|
|
||||||
- name: Send Slack Failure Notification
|
|
||||||
if: fromJSON(steps.run-status.outputs.result).current == 'failure'
|
|
||||||
uses: rtCamp/action-slack-notify@v2
|
|
||||||
env:
|
|
||||||
SLACK_CHANNEL: ${{ inputs.channel }}
|
|
||||||
SLACK_COLOR: danger
|
|
||||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
|
||||||
SLACK_TITLE: 'Failed'
|
|
||||||
SLACK_MESSAGE: ${{ env.SUBJECT }}
|
|
||||||
SLACK_USERNAME: 'EAS CLI CI'
|
|
||||||
SLACK_WEBHOOK: ${{ inputs.slack_webhook }}
|
|
||||||
SLACK_FOOTER: ''
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
function updateOctokitInstance(github) {
|
|
||||||
updateGithubRestActions(github);
|
|
||||||
addConvenienceMethods(github);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addConvenienceMethods(github) {
|
|
||||||
Object.assign(github, {
|
|
||||||
// checks if a run is a failure from the perspective of sending Slack
|
|
||||||
// notifications. Works for both runs and run attempts, which should be the
|
|
||||||
// same object shapes.
|
|
||||||
// When a run has not been completed, it fetches the jobs for the workflow
|
|
||||||
// to check if the job is "failing" instead of "failed".
|
|
||||||
async isFailedRun(run) {
|
|
||||||
console.log(`checking run: ${run.id}`);
|
|
||||||
console.log(`status: ${run.status}`);
|
|
||||||
console.log(`conclusion: ${run.conclusion}`);
|
|
||||||
console.log(`run_attempt: ${run.run_attempt}`);
|
|
||||||
|
|
||||||
if (run.status === 'completed') {
|
|
||||||
console.log('completed -- checking conclusion');
|
|
||||||
return run.conclusion === 'failure' || run.conclusion === 'timed_out';
|
|
||||||
}
|
|
||||||
|
|
||||||
const owner = run.repository.owner.login;
|
|
||||||
const repo = run.repository.name;
|
|
||||||
const jobsResp = await github.rest.actions.listJobsForWorkflowRun({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
run_id: run.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('checking jobs');
|
|
||||||
const jobs = jobsResp.data;
|
|
||||||
for (const job of jobs.jobs) {
|
|
||||||
if (job.conclusion === 'failure' || job.conclusion === 'timed_out') {
|
|
||||||
console.log('job failed');
|
|
||||||
return true;
|
|
||||||
} else if (job.status === 'completed') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const step of job.steps) {
|
|
||||||
if (step.conclusion === 'failure') {
|
|
||||||
console.log('job step failed');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// github-script's Octokit/REST plugins are out of date, so we have to
|
|
||||||
// add our own custom endpoint method
|
|
||||||
// see https://octokit.github.io/rest.js/v18#custom-endpoint-methods for
|
|
||||||
// documentation
|
|
||||||
function updateGithubRestActions(github) {
|
|
||||||
Object.assign(github.rest.actions, {
|
|
||||||
getWorkflowRunAttempt: github.request.defaults({
|
|
||||||
method: 'GET',
|
|
||||||
url: '/repos/{owner}/{repo}/actions/runs/{run_id}/attempts/{attempt_number}',
|
|
||||||
params: {
|
|
||||||
owner: {
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
repo: {
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
},
|
|
||||||
run_id: {
|
|
||||||
required: true,
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
attempt_number: {
|
|
||||||
required: true,
|
|
||||||
type: 'number',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns { current: 'failure' } | { current: 'success', previous: 'success' | 'failure' } to indicate the failure status
|
|
||||||
// of the current and previous runs
|
|
||||||
module.exports = async ({ github }) => {
|
|
||||||
const { GITHUB_REPOSITORY, GITHUB_RUN_ID, GITHUB_REF_NAME } = process.env;
|
|
||||||
const [owner, repo] = GITHUB_REPOSITORY.split('/');
|
|
||||||
updateOctokitInstance(github);
|
|
||||||
|
|
||||||
const workflowRunResp = await github.rest.actions.getWorkflowRun({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
run_id: GITHUB_RUN_ID,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentRun = workflowRunResp.data;
|
|
||||||
const listWorkflowRunsResp = await github.rest.actions.listWorkflowRuns({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
workflow_id: currentRun.workflow_id,
|
|
||||||
branch: GITHUB_REF_NAME,
|
|
||||||
});
|
|
||||||
const { workflow_runs } = listWorkflowRunsResp.data;
|
|
||||||
|
|
||||||
if (workflow_runs.length === 1 && workflow_runs[0].run_attempt === 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousRuns = workflow_runs.filter(
|
|
||||||
(run) => run.status === 'completed' && run.conclusion !== 'cancelled'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await github.isFailedRun(currentRun)) {
|
|
||||||
return { current: 'failure', previous: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentRun.run_attempt === 1 && previousRuns.length > 0) {
|
|
||||||
return {
|
|
||||||
current: 'success',
|
|
||||||
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = currentRun.run_attempt - 1; i > 0; i--) {
|
|
||||||
const previousAttemptResp = await github.rest.actions.getWorkflowRunAttempt({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
run_id: currentRun.id,
|
|
||||||
attempt_number: i,
|
|
||||||
});
|
|
||||||
const previousAttempt = previousAttemptResp.data;
|
|
||||||
if (
|
|
||||||
previousAttempt.conclusion === 'success' ||
|
|
||||||
previousAttempt.conclusion === 'failure' ||
|
|
||||||
previousAttempt.conclusion === 'timed_out'
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
current: 'success',
|
|
||||||
previous: (await github.isFailedRun(previousAttempt)) ? 'failure' : 'success',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousRuns.length > 0) {
|
|
||||||
return {
|
|
||||||
current: 'success',
|
|
||||||
previous: (await github.isFailedRun(previousRuns[0])) ? 'failure' : 'success',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { current: 'success', previous: 'success' };
|
|
||||||
};
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "notify-slack-on-fail-or-recover",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "A GitHub Action to notify Expo Slack of build failures and recovery.",
|
|
||||||
"main": "main.js",
|
|
||||||
"author": "Expo",
|
|
||||||
"license": "UNLICENSED",
|
|
||||||
"private": true,
|
|
||||||
"devDependencies": {
|
|
||||||
"prettier": "^2.5.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
|
||||||
# yarn lockfile v1
|
|
||||||
|
|
||||||
|
|
||||||
[email protected]:
|
|
||||||
version "2.5.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a"
|
|
||||||
integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name: Changelog bot
|
|
||||||
|
|
||||||
on:
|
|
||||||
issue_comment:
|
|
||||||
types: [created]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pr-commented:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
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: 20
|
|
||||||
- name: Checkout Pull Request
|
|
||||||
run: gh pr checkout ${{ github.event.issue.number }}
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- name: Install dependencies
|
|
||||||
working-directory: ./scripts
|
|
||||||
run: yarn install --frozen-lockfile --check-files
|
|
||||||
- name: Add changelog entry
|
|
||||||
working-directory: ./scripts
|
|
||||||
run: |
|
|
||||||
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
|
||||||
if [[ ! "$PARAMS" =~ " " ]]; then
|
|
||||||
echo "Invalid /changelog-entry command"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
|
||||||
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
|
||||||
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
|
||||||
env:
|
|
||||||
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
|
||||||
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
|
||||||
- name: Commit and push
|
|
||||||
run: |
|
|
||||||
# use the last commit author name and email
|
|
||||||
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
|
||||||
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
|
||||||
|
|
||||||
git add CHANGELOG.md
|
|
||||||
git commit -m "update CHANGELOG.md"
|
|
||||||
git push
|
|
||||||
@@ -1,41 +1,46 @@
|
|||||||
name: CHANGELOG.md PR check
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
issue_comment:
|
||||||
types:
|
types: [created]
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- labeled
|
|
||||||
- unlabeled
|
|
||||||
|
|
||||||
concurrency:
|
name: Changelog
|
||||||
group: changelog-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pr-changelog-entry:
|
pr-commented:
|
||||||
name: Check CHANGELOG.md updated
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.issue.pull_request && github.event.issue.user.login == github.event.comment.user.login && startsWith(github.event.comment.body, '/changelog-entry ') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Check if CHANGELOG.md was updated
|
- uses: actions/setup-node@v2
|
||||||
id: changelog-updated
|
|
||||||
uses: tj-actions/changed-files@v41
|
|
||||||
with:
|
with:
|
||||||
files: CHANGELOG.md
|
node-version: 18
|
||||||
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
|
- name: Checkout Pull Request
|
||||||
if: steps.changelog-updated.outputs.any_changed == 'false' && !contains(github.event.pull_request.labels.*.name, 'no changelog')
|
run: hub pr checkout ${{ github.event.issue.number }}
|
||||||
uses: actions/github-script@v6
|
env:
|
||||||
with:
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
- name: Install dependencies
|
||||||
core.setFailed('Please add a changelog entry!')
|
working-directory: ./scripts
|
||||||
- uses: mshick/[email protected]
|
run: yarn install --frozen-lockfile --check-files
|
||||||
if: always()
|
- name: Add changelog entry
|
||||||
with:
|
working-directory: ./scripts
|
||||||
message-id: changelog-entry-check
|
run: |
|
||||||
refresh-message-position: true
|
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
|
||||||
message: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog') && '⏩ The changelog entry check has been skipped since the "no changelog" label is present.' || '✅ Thank you for adding the changelog entry!' }}
|
if [[ ! "$PARAMS" =~ " " ]]; then
|
||||||
message-failure: |
|
echo "Invalid /changelog-entry command"
|
||||||
❌ It looks like a changelog entry is missing for this PR. You have two options: you can add it manually, or you can use the changelog bot to do it for you.
|
exit 1
|
||||||
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
|
fi
|
||||||
⏩ If this PR doesn't require a changelog entry, such as if it's an internal change that doesn't affect the user experience, you can add the "no changelog" label to the PR.
|
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||||
|
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
|
||||||
|
yarn changelog-entry "$CATEGORY" "$MESSAGE"
|
||||||
|
env:
|
||||||
|
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
|
||||||
|
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
PR_COMMENT_BODY: ${{ github.event.comment.body }}
|
||||||
|
- name: Commit and push
|
||||||
|
run: |
|
||||||
|
# use the last commit author name and email
|
||||||
|
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
|
||||||
|
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
|
||||||
|
|
||||||
|
git add CHANGELOG.md
|
||||||
|
git commit -m "update CHANGELOG.md"
|
||||||
|
git push
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- run: yarn install --frozen-lockfile --check-files
|
- run: yarn install --frozen-lockfile --check-files
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- name: Build tarballs
|
- name: Build tarballs
|
||||||
@@ -36,10 +36,10 @@ jobs:
|
|||||||
yarn pretarball-ci
|
yarn pretarball-ci
|
||||||
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
yarn oclif pack:tarballs --no-xz --targets linux-x64,linux-arm
|
||||||
- id: x64
|
- id: x64
|
||||||
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||||
- id: arm
|
- id: arm
|
||||||
run: echo "filename=$(ls eas-*-arm.tar.gz)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=filename::$(ls eas-*-arm.tar.gz)"
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||||
- name: Upload linux-x64 tarball
|
- name: Upload linux-x64 tarball
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -68,22 +68,16 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- run: brew install python-setuptools
|
|
||||||
env:
|
|
||||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
|
||||||
- run: yarn install --frozen-lockfile --check-files
|
- run: yarn install --frozen-lockfile --check-files
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
- name: Build tarballs
|
- name: Build tarballs
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||||
run: |
|
run: |
|
||||||
yarn pretarball-ci
|
yarn pretarball-ci
|
||||||
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64
|
yarn oclif pack:tarballs --no-xz --targets darwin-x64
|
||||||
- id: x64
|
- id: x64
|
||||||
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=filename::$(ls eas-*-x64.tar.gz)"
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
|
||||||
- id: arm64
|
|
||||||
run: echo "filename=$(ls eas-*-arm64.tar.gz)" >> $GITHUB_OUTPUT
|
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
working-directory: ${{ github.workspace }}/packages/eas-cli/dist
|
||||||
- name: Upload darwin-x64 tarball
|
- name: Upload darwin-x64 tarball
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -94,15 +88,6 @@ jobs:
|
|||||||
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
|
||||||
asset_name: ${{ steps.x64.outputs.filename }}
|
asset_name: ${{ steps.x64.outputs.filename }}
|
||||||
asset_content_type: application/gzip
|
asset_content_type: application/gzip
|
||||||
- name: Upload darwin-arm64 tarball
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
|
||||||
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
|
|
||||||
asset_name: ${{ steps.arm64.outputs.filename }}
|
|
||||||
asset_content_type: application/gzip
|
|
||||||
build-windows:
|
build-windows:
|
||||||
name: Build for Windows
|
name: Build for Windows
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -111,7 +96,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- run: yarn install --frozen-lockfile --check-files
|
- run: yarn install --frozen-lockfile --check-files
|
||||||
- run: sudo apt-get install nsis
|
- run: sudo apt-get install nsis
|
||||||
- run: yarn build
|
- run: yarn build
|
||||||
@@ -120,10 +105,10 @@ jobs:
|
|||||||
yarn oclif pack:win
|
yarn oclif pack:win
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||||
- id: x64
|
- id: x64
|
||||||
run: echo "filename=$(ls eas-*-x64.exe)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=filename::$(ls eas-*-x64.exe)"
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||||
- id: x86
|
- id: x86
|
||||||
run: echo "filename=$(ls eas-*-x86.exe)" >> $GITHUB_OUTPUT
|
run: echo "::set-output name=filename::$(ls eas-*-x86.exe)"
|
||||||
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
working-directory: ${{ github.workspace }}/packages/eas-cli/dist/win32
|
||||||
- name: Upload x64 installer
|
- name: Upload x64 installer
|
||||||
uses: actions/upload-release-asset@v1
|
uses: actions/upload-release-asset@v1
|
||||||
@@ -153,7 +138,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
registry-url: 'https://registry.npmjs.org/'
|
registry-url: 'https://registry.npmjs.org/'
|
||||||
scope: 'expo'
|
scope: 'expo'
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: yarn install --frozen-lockfile --check-files
|
run: yarn install --frozen-lockfile --check-files
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -162,31 +147,15 @@ jobs:
|
|||||||
run: yarn lerna publish from-package --yes --no-verify-access
|
run: yarn lerna publish from-package --yes --no-verify-access
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
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
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
|
||||||
- name: Add latest-eas-build 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
|
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
|
||||||
release-changelog:
|
release-changelog:
|
||||||
name: Update changelog and publish release
|
name: Update changelog and publish release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: publish-to-npm
|
needs: publish-to-npm
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
with:
|
|
||||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
working-directory: ./scripts
|
working-directory: ./scripts
|
||||||
run: yarn install --frozen-lockfile --check-files
|
run: yarn install --frozen-lockfile --check-files
|
||||||
@@ -196,7 +165,7 @@ jobs:
|
|||||||
working-directory: ./scripts
|
working-directory: ./scripts
|
||||||
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
run: gh release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
@@ -220,22 +189,9 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SLACK_CHANNEL: eas-cli
|
SLACK_CHANNEL: eas-cli
|
||||||
SLACK_COLOR: ${{ job.status }}
|
SLACK_COLOR: ${{ job.status }}
|
||||||
SLACK_ICON: https://github.com/expo-bot.png?size=48
|
SLACK_ICON: https://github.com/expo-ci.png?size=48
|
||||||
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
SLACK_TITLE: 'Released eas-cli@${{ env.EAS_CLI_VERSION }}'
|
||||||
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
SLACK_MESSAGE: ${{ env.SLACK_CHANGELOG }}
|
||||||
SLACK_USERNAME: 'EAS CLI CI'
|
SLACK_USERNAME: 'Expo CI'
|
||||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||||
SLACK_FOOTER: ''
|
SLACK_FOOTER: ''
|
||||||
open-pr-in-universe:
|
|
||||||
name: Open a PR with `@expo/eas-json` bump in universe
|
|
||||||
needs: publish-to-npm
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Open a PR with `@expo/eas-json` version bump in universe
|
|
||||||
uses: benc-uk/workflow-dispatch@v1
|
|
||||||
with:
|
|
||||||
repo: expo/universe
|
|
||||||
ref: main
|
|
||||||
workflow: update-eas-json.yml
|
|
||||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
|
||||||
@@ -2,9 +2,6 @@ name: Size
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [synchronize, opened]
|
types: [synchronize, opened]
|
||||||
concurrency:
|
|
||||||
group: size-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
jobs:
|
jobs:
|
||||||
compare:
|
compare:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -12,7 +9,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- uses: preactjs/compressed-size-action@v2
|
- uses: preactjs/compressed-size-action@v2
|
||||||
with:
|
with:
|
||||||
build-script: 'build:tarballs:linux'
|
build-script: 'build:tarballs:linux'
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- 'scripts/**'
|
- 'scripts/**'
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: test-scripts-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -25,6 +21,6 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 18
|
||||||
- run: yarn install --frozen-lockfile --check-files
|
- run: yarn install --frozen-lockfile --check-files
|
||||||
- run: yarn typecheck
|
- run: yarn typecheck
|
||||||
@@ -1,23 +1,18 @@
|
|||||||
name: Run tests
|
name: Run tests
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: test-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: ['18', '20', '22']
|
node: ['14', '16', '18']
|
||||||
include:
|
include:
|
||||||
- node: '20'
|
- node: '18'
|
||||||
coverage: true
|
coverage: true
|
||||||
name: Test with Node ${{ matrix.node }}
|
name: Test with Node ${{ matrix.node }}
|
||||||
steps:
|
steps:
|
||||||
@@ -30,32 +25,14 @@ jobs:
|
|||||||
- run: yarn typecheck
|
- run: yarn typecheck
|
||||||
- run: yarn test
|
- run: yarn test
|
||||||
if: ${{ !matrix.coverage }}
|
if: ${{ !matrix.coverage }}
|
||||||
env:
|
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
|
||||||
- run: yarn test --coverage
|
- run: yarn test --coverage
|
||||||
if: ${{ matrix.coverage }}
|
if: ${{ matrix.coverage }}
|
||||||
env:
|
env:
|
||||||
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
# Oclif seems to cause OOM errors when testing with coverage enabled.
|
||||||
# Remove this whenever we solved those OOM errors.
|
# Remove this whenever we solved those OOM errors.
|
||||||
NODE_OPTIONS: --max-old-space-size=8192
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
- run: yarn lint --max-warnings=0
|
- run: yarn lint --max-warnings=0
|
||||||
- run: yarn install --frozen-lockfile --check-files
|
|
||||||
working-directory: ./scripts
|
|
||||||
- uses: codecov/codecov-action@v1
|
- uses: codecov/codecov-action@v1
|
||||||
if: ${{ matrix.coverage }}
|
if: ${{ matrix.coverage }}
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
- run: yarn lint-changelog
|
|
||||||
|
|
||||||
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 }}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
name: Open a PR bumping eas-build modules
|
|
||||||
|
|
||||||
on: workflow_dispatch
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
open-pr:
|
|
||||||
name: Open a PR bumping eas-build modules
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
|
||||||
|
|
||||||
- name: Setup node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
|
|
||||||
- run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Configure git
|
|
||||||
run: |
|
|
||||||
git config --global user.email "[email protected]"
|
|
||||||
git config --global user.name "Expo Bot"
|
|
||||||
|
|
||||||
- name: Upgrade eas-build modules
|
|
||||||
run: |
|
|
||||||
for DIR in packages/eas-cli packages/eas-json; do
|
|
||||||
pushd $DIR >/dev/null 2>&1
|
|
||||||
# packages are ordered according to output of `lerna list --toposort`
|
|
||||||
# https://github.com/expo/turtle-v2/pull/568
|
|
||||||
for PACKAGE in "@expo/downloader" "@expo/eas-build-job" "@expo/logger" "@expo/template-file" "@expo/steps" "@expo/turtle-spawn" "@expo/build-tools"; do
|
|
||||||
if grep -q $PACKAGE package.json; then
|
|
||||||
echo "[$DIR] yarn --exact add $PACKAGE"
|
|
||||||
yarn add --exact $PACKAGE
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
popd >/dev/null 2>&1
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Set a branch name
|
|
||||||
id: bump-branch-name
|
|
||||||
run: |
|
|
||||||
HASH="$(echo $RANDOM | md5sum | head -c 10)"
|
|
||||||
BRANCH_NAME="@expo-bot/eas-build-bump-$HASH"
|
|
||||||
|
|
||||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Create a branch, commit, and push
|
|
||||||
run: |
|
|
||||||
BRANCH_NAME="${{ steps.bump-branch-name.outputs.BRANCH_NAME }}"
|
|
||||||
|
|
||||||
git checkout -b $BRANCH_NAME
|
|
||||||
git add .
|
|
||||||
git commit -m "Upgrade eas-build dependencies"
|
|
||||||
git push origin $BRANCH_NAME
|
|
||||||
|
|
||||||
- name: Open a PR to main
|
|
||||||
uses: repo-sync/pull-request@v2
|
|
||||||
id: open-pr
|
|
||||||
with:
|
|
||||||
source_branch: '${{ steps.bump-branch-name.outputs.BRANCH_NAME }}'
|
|
||||||
destination_branch: 'main'
|
|
||||||
pr_title: 'Upgrade eas-build dependencies'
|
|
||||||
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
|
|
||||||
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
|
|
||||||
github_token: ${{ secrets.EXPO_BOT_PAT }}
|
|
||||||
|
|
||||||
- uses: actions/github-script@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.EXPO_BOT_PAT }}
|
|
||||||
script: |
|
|
||||||
github.issues.createComment({
|
|
||||||
issue_number: ${{ steps.open-pr.outputs.pr_number }},
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
body: "/changelog-entry chore Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies."
|
|
||||||
})
|
|
||||||
Vendored
-10
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"eslint.enable": true,
|
|
||||||
"search.exclude": {
|
|
||||||
"**/node_modules": true,
|
|
||||||
"**/bower_components": true,
|
|
||||||
"**/*.code-search": true,
|
|
||||||
"**/src/**/build/**": false,
|
|
||||||
"**/src/build/**": false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+30
-1249
File diff suppressed because it is too large.
Load diff
+8
-8
@@ -1,16 +1,16 @@
|
|||||||
* @szdziedzic @khamilowicz @sjchmiela
|
* @dsokal @wkozyra95
|
||||||
|
|
||||||
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
|
packages/eas-cli/src/build @dsokal @wkozyra95
|
||||||
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
|
packages/eas-cli/src/commands/build @dsokal @wkozyra95
|
||||||
|
|
||||||
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
|
packages/eas-cli/src/submit @dsokal
|
||||||
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
|
packages/eas-cli/src/commands/submit.ts @dsokal
|
||||||
|
|
||||||
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
|
packages/eas-cli/src/update @kgc00 @wschurman
|
||||||
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
|
packages/eas-cli/src/commands/update @kgc00 @wschurman
|
||||||
|
|
||||||
packages/eas-cli/schema @bycedric
|
packages/eas-cli/schema @bycedric
|
||||||
packages/eas-cli/src/metadata @bycedric
|
packages/eas-cli/src/metadata @bycedric
|
||||||
packages/eas-cli/src/commands/metadata @bycedric
|
packages/eas-cli/src/commands/metadata @bycedric
|
||||||
|
|
||||||
packages/eas-json @szdziedzic @khamilowicz @sjchmiela
|
packages/eas-json @dsokal @wkozyra95
|
||||||
+3
-66
@@ -1,67 +1,4 @@
|
|||||||
# Development
|
## Releasing a new version
|
||||||
|
|
||||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
1. Run `yarn release` in the repository root folder.
|
||||||
|
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
|
||||||
|
|
||||||
Open your `.zshrc` or other config file and add:
|
|
||||||
|
|
||||||
```
|
|
||||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use it with `easd` like `easd build`.
|
|
||||||
|
|
||||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn start
|
|
||||||
```
|
|
||||||
|
|
||||||
If it is easier for you to work with the code locally with compiler options set to:
|
|
||||||
|
|
||||||
```json
|
|
||||||
...
|
|
||||||
"compilerOptions": {
|
|
||||||
...
|
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noUnusedParameters": false,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
you can consider using:
|
|
||||||
|
|
||||||
```
|
|
||||||
yarn start-allow-unused
|
|
||||||
```
|
|
||||||
|
|
||||||
## Format
|
|
||||||
|
|
||||||
- Be sure to update the [`CHANGELOG.md`](./CHANGELOG.md) with changes for every PR. There is a changelog bot that can generate the proper entry for you. The instructions on how to use it are in the PR description placeholder.
|
|
||||||
- End `async` functions with `Async` like `runAsync`. This is just how we format functions at Expo.
|
|
||||||
- Utilize the unified `Log` module instead of `console.log`.
|
|
||||||
|
|
||||||
## Using local and staging API
|
|
||||||
|
|
||||||
For development against local API:
|
|
||||||
|
|
||||||
- Set up the API server as described at https://github.com/expo/universe/tree/main/server/www.
|
|
||||||
- Set the `EXPO_LOCAL=1` env variable either on every command run (e.g. `EXPO_LOCAL=1 easd ...`) or more permamently with `export EXPO_LOCAL=1` (works in a current shell session).
|
|
||||||
|
|
||||||
For development against staging API:
|
|
||||||
|
|
||||||
- Set the `EXPO_STAGING=1` env variable either on every command run (e.g. `EXPO_STAGING=1 easd ...`) or more permamently with `export EXPO_STAGING=1` (works in a current shell session).
|
|
||||||
|
|
||||||
## Working on local builds (`eas build --local`)
|
|
||||||
|
|
||||||
See https://github.com/expo/eas-build/blob/main/DEVELOPMENT.md for how to set up your environment when making changes to [`eas-cli-local-build-plugin`](https://github.com/expo/eas-build/tree/main/packages/local-build-plugin) and/or [`build-tools`](https://github.com/expo/eas-build/tree/main/packages/build-tools).
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Run `yarn test` either in the repository root or in a package directory that you're working on.
|
|
||||||
|
|
||||||
## Releasing
|
|
||||||
|
|
||||||
See [RELEASING.md](./RELEASING.md).
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Releasing EAS CLI
|
|
||||||
|
|
||||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
|
||||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
|
||||||
|
|
||||||
## Choosing the next version
|
|
||||||
|
|
||||||
As stated above, the next EAS CLI version is chosen automatically based on the existing changelog entries under the unreleased section in **CHANGELOG.md**.
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Security Policy
|
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
|
||||||
|
|
||||||
Instead of opening a GitHub issue for security vulnerabilities, send an email to our secure inbox: secure@expo.io.
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
require('./packages/eas-cli/jest.config.js'),
|
||||||
|
require('./packages/eas-json/jest.config.js'),
|
||||||
|
],
|
||||||
|
testPathIgnorePatterns: ['.*'],
|
||||||
|
};
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { Config } from 'jest';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
|
||||||
import easCliConfig from './packages/eas-cli/jest.config';
|
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
|
||||||
import easJsonConfig from './packages/eas-json/jest.config';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
projects: [easCliConfig, easJsonConfig],
|
|
||||||
testPathIgnorePatterns: ['.*'],
|
|
||||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
|
||||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
||||||
|
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||||
|
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||||
|
};
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { Config } from 'jest';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
testRegex: '/__tests__/.*(test|spec)\\.[jt]sx?$',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"version": "10.0.2",
|
"useWorkspaces": true,
|
||||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
"version": "2.1.0"
|
||||||
}
|
}
|
||||||
+16
-19
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "eas-cli-root",
|
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@@ -9,33 +8,31 @@
|
|||||||
"build:tarballs:linux": "yarn build-for-size-check",
|
"build:tarballs:linux": "yarn build-for-size-check",
|
||||||
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
|
||||||
"typecheck": "lerna run typecheck",
|
"typecheck": "lerna run typecheck",
|
||||||
"start": "lerna run watch --stream --parallel",
|
"start": "lerna run watch --parallel",
|
||||||
"start-allow-unused": "lerna run watch-allow-unused --stream --parallel",
|
|
||||||
"watch": "yarn start",
|
"watch": "yarn start",
|
||||||
"eas": "packages/eas-cli/bin/run",
|
"eas": "packages/eas-cli/bin/run",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint-changelog": "./scripts/bin/run lint-changelog",
|
"release": "./scripts/bin/run update-local-plugin && lerna version --exact",
|
||||||
"release": "lerna run rebuild && ./scripts/bin/run release",
|
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"clean": "lerna run clean && rm -rf node_modules coverage"
|
"clean": "lerna run clean && rm -rf node_modules coverage"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "29.5.11",
|
"@types/jest": "28.1.6",
|
||||||
"@types/node": "20.11.0",
|
"@types/node": "18.6.5",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.21.0",
|
||||||
"eslint-config-universe": "12.0.0",
|
"eslint-config-universe": "11.1.0",
|
||||||
"eslint-plugin-async-protect": "3.1.0",
|
"eslint-plugin-async-protect": "2.1.0",
|
||||||
"eslint-plugin-node": "11.1.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"jest": "29.7.0",
|
"jest": "28.1.3",
|
||||||
"jest-watch-typeahead": "2.2.2",
|
"jest-watch-typeahead": "2.0.0",
|
||||||
"lerna": "7.4.2",
|
"lerna": "4.0.0",
|
||||||
"oclif": "^3.9.0",
|
"oclif": "3.1.2",
|
||||||
"prettier": "3.1.1",
|
"prettier": "2.7.1",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "28.0.7",
|
||||||
"typescript": "5.3.3"
|
"typescript": "4.7.4"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.11.0",
|
"node": "18.6.0",
|
||||||
"yarn": "1.22.21"
|
"yarn": "1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+174
-606
File diff suppressed because it is too large.
Load diff
@@ -5,8 +5,6 @@ documents:
|
|||||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||||
- 'src/commands/**/*.ts'
|
- 'src/commands/**/*.ts'
|
||||||
- 'src/branch/**/*.ts'
|
|
||||||
- 'src/channel/**/*.ts'
|
|
||||||
generates:
|
generates:
|
||||||
src/graphql/generated.ts:
|
src/graphql/generated.ts:
|
||||||
plugins:
|
plugins:
|
||||||
|
|||||||
Generated
+3567
-18153
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,7 @@
|
|||||||
|
module.exports = {
|
||||||
|
...require('../../jest/jest.config.js'),
|
||||||
|
rootDir: __dirname,
|
||||||
|
roots: ['src', '__mocks__'],
|
||||||
|
globalSetup: './jest/global-setup.ts',
|
||||||
|
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
||||||
|
};
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import type { Config } from 'jest';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/no-relative-packages
|
|
||||||
import baseConfig from '../../jest/jest.config';
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
...baseConfig,
|
|
||||||
rootDir: __dirname,
|
|
||||||
roots: ['src', '__mocks__'],
|
|
||||||
globalSetup: './jest/global-setup.ts',
|
|
||||||
setupFilesAfterEnv: ['./jest/setup-after-env.ts'],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -2,5 +2,4 @@ module.exports = async () => {
|
|||||||
process.env.TZ = 'UTC';
|
process.env.TZ = 'UTC';
|
||||||
process.env.EAS_NO_VCS = '1';
|
process.env.EAS_NO_VCS = '1';
|
||||||
process.env.EAS_PROJECT_ROOT = '/app';
|
process.env.EAS_PROJECT_ROOT = '/app';
|
||||||
process.env.FORCE_COLOR = '1';
|
|
||||||
};
|
};
|
||||||
@@ -1,127 +1,116 @@
|
|||||||
{
|
{
|
||||||
"name": "eas-cli",
|
"name": "eas-cli",
|
||||||
"description": "EAS command line tool",
|
"description": "EAS command line tool",
|
||||||
"version": "10.0.2",
|
"version": "2.1.0",
|
||||||
"author": "Expo <[email protected]>",
|
"author": "Expo <[email protected]>",
|
||||||
"bin": {
|
"bin": {
|
||||||
"eas": "./bin/run"
|
"eas": "./bin/run"
|
||||||
},
|
},
|
||||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/apple-utils": "1.7.0",
|
"@expo/apple-utils": "0.0.0-alpha.34",
|
||||||
"@expo/code-signing-certificates": "0.0.5",
|
"@expo/code-signing-certificates": "0.0.2",
|
||||||
"@expo/config": "8.5.4",
|
"@expo/config": "7.0.1",
|
||||||
"@expo/config-plugins": "7.8.4",
|
"@expo/config-plugins": "5.0.1",
|
||||||
"@expo/config-types": "50.0.0",
|
"@expo/config-types": "46.0.2",
|
||||||
"@expo/eas-build-job": "1.0.117",
|
"@expo/eas-build-job": "0.2.94",
|
||||||
"@expo/eas-json": "10.0.0",
|
"@expo/eas-json": "2.1.0",
|
||||||
"@expo/json-file": "8.2.37",
|
"@expo/json-file": "8.2.36",
|
||||||
"@expo/logger": "1.0.117",
|
|
||||||
"@expo/multipart-body-parser": "1.1.0",
|
"@expo/multipart-body-parser": "1.1.0",
|
||||||
"@expo/osascript": "2.0.33",
|
"@expo/package-manager": "0.0.56",
|
||||||
"@expo/package-manager": "1.1.2",
|
|
||||||
"@expo/pkcs12": "0.0.8",
|
"@expo/pkcs12": "0.0.8",
|
||||||
"@expo/plist": "0.0.20",
|
"@expo/plist": "0.0.18",
|
||||||
"@expo/plugin-help": "5.1.23",
|
"@expo/plugin-autocomplete": "1.4.0",
|
||||||
"@expo/plugin-warn-if-update-available": "2.5.1",
|
"@expo/plugin-help": "5.3.0",
|
||||||
"@expo/prebuild-config": "6.7.3",
|
"@expo/plugin-warn-if-update-available": "2.3.0",
|
||||||
|
"@expo/prebuild-config": "5.0.3",
|
||||||
"@expo/results": "1.0.0",
|
"@expo/results": "1.0.0",
|
||||||
"@expo/rudder-sdk-node": "1.1.1",
|
"@expo/rudder-sdk-node": "1.1.1",
|
||||||
"@expo/spawn-async": "1.7.0",
|
"@expo/spawn-async": "1.7.0",
|
||||||
"@expo/steps": "1.0.118",
|
|
||||||
"@expo/timeago.js": "1.0.0",
|
"@expo/timeago.js": "1.0.0",
|
||||||
"@oclif/core": "^1.26.2",
|
"@oclif/core": "1.13.10",
|
||||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
"@urql/core": "2.6.1",
|
||||||
"@segment/ajv-human-errors": "^2.1.2",
|
"@urql/exchange-retry": "0.3.3",
|
||||||
"@urql/core": "4.0.11",
|
"ajv": "^6.12.6",
|
||||||
"@urql/exchange-retry": "1.2.0",
|
|
||||||
"ajv": "8.11.0",
|
|
||||||
"ajv-formats": "2.1.1",
|
|
||||||
"better-opn": "3.0.2",
|
"better-opn": "3.0.2",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"cli-progress": "3.12.0",
|
"cli-progress": "3.11.2",
|
||||||
|
"cli-table3": "0.6.2",
|
||||||
"dateformat": "4.6.3",
|
"dateformat": "4.6.3",
|
||||||
"dotenv": "16.3.1",
|
|
||||||
"env-paths": "2.2.0",
|
"env-paths": "2.2.0",
|
||||||
"envinfo": "7.11.0",
|
"envinfo": "7.8.1",
|
||||||
"fast-deep-equal": "3.1.3",
|
"fast-deep-equal": "3.1.3",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.2.11",
|
||||||
"figures": "3.2.0",
|
"figures": "3.2.0",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "10.1.0",
|
||||||
"getenv": "1.0.0",
|
"getenv": "1.0.0",
|
||||||
"gradle-to-js": "2.0.1",
|
"gradle-to-js": "2.0.1",
|
||||||
"graphql": "16.8.1",
|
"graphql": "16.5.0",
|
||||||
"graphql-tag": "2.12.6",
|
"graphql-tag": "2.12.6",
|
||||||
"https-proxy-agent": "5.0.1",
|
"https-proxy-agent": "5.0.1",
|
||||||
"ignore": "5.3.0",
|
"ignore": "5.2.0",
|
||||||
"indent-string": "4.0.0",
|
"indent-string": "4.0.0",
|
||||||
"jks-js": "1.1.0",
|
"jks-js": "1.0.2",
|
||||||
"joi": "17.11.0",
|
"joi": "17.6.0",
|
||||||
"jsonwebtoken": "9.0.0",
|
"keychain": "1.3.0",
|
||||||
"keychain": "1.5.0",
|
|
||||||
"log-symbols": "4.1.0",
|
"log-symbols": "4.1.0",
|
||||||
"mime": "3.0.0",
|
"mime": "3.0.0",
|
||||||
"minimatch": "5.1.2",
|
"minimatch": "5.1.0",
|
||||||
"nanoid": "3.3.4",
|
"nanoid": "3.3.4",
|
||||||
"node-fetch": "2.6.7",
|
"node-fetch": "2.6.7",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"nullthrows": "1.1.1",
|
"nullthrows": "1.1.1",
|
||||||
"ora": "5.1.0",
|
"ora": "5.1.0",
|
||||||
"pkg-dir": "4.2.0",
|
"pkg-dir": "4.2.0",
|
||||||
"pngjs": "7.0.0",
|
|
||||||
"promise-limit": "2.7.0",
|
"promise-limit": "2.7.0",
|
||||||
"promise-retry": "2.0.1",
|
"promise-retry": "2.0.1",
|
||||||
"prompts": "2.4.2",
|
"prompts": "2.4.2",
|
||||||
"qrcode-terminal": "0.12.0",
|
"qrcode-terminal": "0.12.0",
|
||||||
"resolve-from": "5.0.0",
|
"resolve-from": "5.0.0",
|
||||||
"semver": "7.5.4",
|
"semver": "7.3.7",
|
||||||
"slash": "3.0.0",
|
"slash": "3.0.0",
|
||||||
"tar": "6.2.1",
|
"tar": "6.1.11",
|
||||||
"terminal-link": "2.1.1",
|
"terminal-link": "2.1.1",
|
||||||
"tslib": "2.6.2",
|
"tslib": "2.4.0",
|
||||||
"turndown": "7.1.2",
|
"turndown": "7.1.1",
|
||||||
"untildify": "4.0.0",
|
"untildify": "4.0.0",
|
||||||
"uuid": "9.0.1",
|
"uuid": "8.3.2",
|
||||||
"wrap-ansi": "7.0.0"
|
"wrap-ansi": "7.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "5.0.0",
|
"@graphql-codegen/cli": "2.11.5",
|
||||||
"@graphql-codegen/introspection": "4.0.0",
|
"@graphql-codegen/introspection": "2.2.1",
|
||||||
"@graphql-codegen/typescript": "4.0.1",
|
"@graphql-codegen/typescript": "2.7.3",
|
||||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
"@graphql-codegen/typescript-operations": "2.5.3",
|
||||||
"@types/cli-progress": "3.11.5",
|
"@types/cli-progress": "3.11.0",
|
||||||
"@types/dateformat": "3.0.1",
|
"@types/dateformat": "3.0.1",
|
||||||
"@types/envinfo": "7.8.3",
|
"@types/envinfo": "7.8.1",
|
||||||
"@types/express": "4.17.15",
|
"@types/fs-extra": "9.0.13",
|
||||||
"@types/fs-extra": "11.0.4",
|
|
||||||
"@types/getenv": "^1.0.0",
|
"@types/getenv": "^1.0.0",
|
||||||
"@types/jsonwebtoken": "8.5.1",
|
"@types/mime": "2.0.3",
|
||||||
"@types/mime": "3.0.1",
|
|
||||||
"@types/node-fetch": "2.6.2",
|
"@types/node-fetch": "2.6.2",
|
||||||
"@types/node-forge": "1.3.1",
|
"@types/node-forge": "1.0.4",
|
||||||
"@types/pngjs": "6.0.4",
|
|
||||||
"@types/promise-retry": "1.1.3",
|
"@types/promise-retry": "1.1.3",
|
||||||
"@types/prompts": "2.4.2",
|
"@types/prompts": "2.0.14",
|
||||||
"@types/semver": "7.5.6",
|
"@types/semver": "7.3.10",
|
||||||
"@types/tar": "6.1.10",
|
"@types/tar": "6.1.2",
|
||||||
"@types/tough-cookie": "4.0.2",
|
"@types/tough-cookie": "4.0.2",
|
||||||
"@types/uuid": "9.0.7",
|
"@types/uuid": "8.3.4",
|
||||||
"@types/wrap-ansi": "3.0.0",
|
"@types/wrap-ansi": "3.0.0",
|
||||||
"axios": "0.27.2",
|
"axios": "0.27.2",
|
||||||
"eslint-plugin-graphql": "4.0.0",
|
"eslint-plugin-graphql": "4.0.0",
|
||||||
"express": "4.19.2",
|
"memfs": "3.4.7",
|
||||||
"memfs": "3.4.13",
|
|
||||||
"mockdate": "3.0.5",
|
"mockdate": "3.0.5",
|
||||||
"nock": "13.4.0",
|
"nock": "13.2.9",
|
||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"ts-deepmerge": "6.2.0",
|
"ts-deepmerge": "2.0.5",
|
||||||
"ts-mockito": "2.6.1",
|
"ts-mockito": "2.6.1",
|
||||||
"ts-node": "10.9.2",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "5.3.3"
|
"typescript": "4.7.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=14.0.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"/bin",
|
"/bin",
|
||||||
@@ -140,7 +129,7 @@
|
|||||||
"bin": "eas",
|
"bin": "eas",
|
||||||
"commands": "./build/commands",
|
"commands": "./build/commands",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@oclif/plugin-autocomplete",
|
"@expo/plugin-autocomplete",
|
||||||
"@expo/plugin-help",
|
"@expo/plugin-help",
|
||||||
"@expo/plugin-warn-if-update-available"
|
"@expo/plugin-warn-if-update-available"
|
||||||
],
|
],
|
||||||
@@ -206,13 +195,10 @@
|
|||||||
"repository": "expo/eas-cli",
|
"repository": "expo/eas-cli",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postpack": "rimraf oclif.manifest.json",
|
"postpack": "rimraf oclif.manifest.json",
|
||||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
"prepack": "rimraf build && yarn build && node ./scripts/prepack.js",
|
||||||
"rebuild": "rimraf build && yarn build",
|
|
||||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||||
"build": "tsc --project tsconfig.build.json",
|
"build": "tsc --project tsconfig.build.json",
|
||||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
|
||||||
"watch": "yarn build --watch --preserveWatchOutput",
|
"watch": "yarn build --watch --preserveWatchOutput",
|
||||||
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
|
|
||||||
"typecheck": "tsc",
|
"typecheck": "tsc",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||||
@@ -220,7 +206,7 @@
|
|||||||
"clean": "rimraf dist build tmp node_modules yarn-error.log"
|
"clean": "rimraf dist build tmp node_modules yarn-error.log"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "20.11.0",
|
"node": "18.6.0",
|
||||||
"yarn": "1.22.21"
|
"yarn": "1.22.19"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,8 +437,8 @@
|
|||||||
"$ref": "#/definitions/apple/AppleKidsAge"
|
"$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-mdn.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.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)"
|
"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)"
|
||||||
},
|
},
|
||||||
"seventeenPlus": {
|
"seventeenPlus": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
@@ -778,7 +778,7 @@
|
|||||||
},
|
},
|
||||||
"properties": {
|
"properties": {
|
||||||
"configVersion": {
|
"configVersion": {
|
||||||
"enum": [0],
|
"const": 0,
|
||||||
"description": "The EAS metadata store configuration version"
|
"description": "The EAS metadata store configuration version"
|
||||||
},
|
},
|
||||||
"apple": {
|
"apple": {
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ const fs = require('fs/promises');
|
|||||||
(async () => {
|
(async () => {
|
||||||
// Patch `oclif readme` path and link generation
|
// Patch `oclif readme` path and link generation
|
||||||
let readmeContent = await fs.readFile('README.md', 'utf8');
|
let readmeContent = await fs.readFile('README.md', 'utf8');
|
||||||
readmeContent = readmeContent.replace(/src\/commands/g, 'packages/eas-cli/src/commands');
|
readmeContent = readmeContent.replace(/\[build\//g, '[src/');
|
||||||
|
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
|
||||||
await fs.writeFile('README.md', readmeContent);
|
await fs.writeFile('README.md', readmeContent);
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { JSONValue } from '@expo/json-file';
|
|
||||||
|
|
||||||
export class ApiV2Error extends Error {
|
|
||||||
override readonly name = 'ApiV2Error';
|
|
||||||
readonly expoApiV2ErrorCode: string;
|
|
||||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
|
||||||
readonly expoApiV2ErrorServerStack?: string;
|
|
||||||
readonly expoApiV2ErrorMetadata?: object;
|
|
||||||
|
|
||||||
constructor(response: {
|
|
||||||
message: string;
|
|
||||||
code: string;
|
|
||||||
stack?: string;
|
|
||||||
details?: JSONValue;
|
|
||||||
metadata?: object;
|
|
||||||
}) {
|
|
||||||
super(response.message);
|
|
||||||
this.expoApiV2ErrorCode = response.code;
|
|
||||||
this.expoApiV2ErrorDetails = response.details;
|
|
||||||
this.expoApiV2ErrorServerStack = response.stack;
|
|
||||||
this.expoApiV2ErrorMetadata = response.metadata;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
|
||||||
import { ApiV2Error } from '../ApiV2Error';
|
import { ApiV2Error, api, getExpoApiBaseUrl } from '../api';
|
||||||
import { ApiV2Client, getExpoApiBaseUrl } from '../api';
|
|
||||||
|
|
||||||
describe(ApiV2Client, () => {
|
describe('apiClient', () => {
|
||||||
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
it('converts Expo APIv2 error to ApiV2Error', async () => {
|
||||||
nock(getExpoApiBaseUrl())
|
nock(getExpoApiBaseUrl())
|
||||||
.post('/v2/test')
|
.post('/v2/test')
|
||||||
@@ -22,9 +21,7 @@ describe(ApiV2Client, () => {
|
|||||||
|
|
||||||
let error: Error | null = null;
|
let error: Error | null = null;
|
||||||
try {
|
try {
|
||||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
await api.postAsync('test', { body: {} });
|
||||||
body: {},
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
@@ -44,9 +41,7 @@ describe(ApiV2Client, () => {
|
|||||||
|
|
||||||
let error: Error | null = null;
|
let error: Error | null = null;
|
||||||
try {
|
try {
|
||||||
await new ApiV2Client({ accessToken: null, sessionSecret: null }).postAsync('test', {
|
await api.postAsync('test', { body: {} });
|
||||||
body: {},
|
|
||||||
});
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error = e;
|
error = e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add [1m{"cli": { "appVersionSource": "remote" }}[22m in eas.json or re-run this command without "--non-interactive" flag."`;
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
|
|
||||||
import BuildVersionGetView from '../../commands/build/version/get';
|
|
||||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
|
||||||
import Log from '../../log';
|
|
||||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
|
||||||
|
|
||||||
jest.mock('../../project/applicationIdentifier');
|
|
||||||
jest.mock('fs');
|
|
||||||
jest.mock('../../log');
|
|
||||||
jest.mock('../../utils/json');
|
|
||||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
|
||||||
|
|
||||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
|
||||||
return {
|
|
||||||
...easJson,
|
|
||||||
cli: {
|
|
||||||
...easJson.cli,
|
|
||||||
appVersionSource: AppVersionSource.REMOTE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(BuildVersionGetView, () => {
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
test('reading version for platform android', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '100',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`);
|
|
||||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
|
||||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reading version for platform android when no remote version is set', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(Log.log).toHaveBeenCalledWith(`No remote versions are configured for this project.`);
|
|
||||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
|
||||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reading version with --json flag for platform android', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '100',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(
|
|
||||||
BuildVersionGetView,
|
|
||||||
['--non-interactive', '--json', '--platform=android'],
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(enableJsonOutput).toHaveBeenCalled();
|
|
||||||
expect(printJsonOnlyOutput).toHaveBeenCalledWith({
|
|
||||||
versionCode: '100',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reading version with --json flag for platform ios', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '100',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(
|
|
||||||
BuildVersionGetView,
|
|
||||||
['--non-interactive', '--json', '--platform=ios'],
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'IOS',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(enableJsonOutput).toHaveBeenCalled();
|
|
||||||
expect(printJsonOnlyOutput).toHaveBeenCalledWith({
|
|
||||||
buildNumber: '100',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reading version with --json flag when no remote version is set', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(
|
|
||||||
BuildVersionGetView,
|
|
||||||
['--non-interactive', '--json', '--platform=android'],
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(enableJsonOutput).toHaveBeenCalled();
|
|
||||||
expect(printJsonOnlyOutput).toHaveBeenCalledWith({});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('reading version when appVersionSource is set to local ', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionGetView, {});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '100',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(
|
|
||||||
BuildVersionGetView,
|
|
||||||
['--non-interactive', '--json', '--platform=android'],
|
|
||||||
ctx
|
|
||||||
);
|
|
||||||
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getMockAppFragment,
|
|
||||||
getMockEasJson,
|
|
||||||
mockCommandContext,
|
|
||||||
mockProjectId,
|
|
||||||
mockTestCommand,
|
|
||||||
} from './utils';
|
|
||||||
import BuildVersionSetView from '../../commands/build/version/set';
|
|
||||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
|
||||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
|
||||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
|
||||||
import Log from '../../log';
|
|
||||||
import * as prompts from '../../prompts';
|
|
||||||
|
|
||||||
jest.mock('../../project/applicationIdentifier');
|
|
||||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
|
||||||
jest.mock('../../graphql/queries/AppQuery');
|
|
||||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
|
||||||
jest.mock('fs');
|
|
||||||
jest.mock('../../log');
|
|
||||||
jest.mock('../../prompts');
|
|
||||||
jest.mock('../../utils/json');
|
|
||||||
|
|
||||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
|
||||||
return {
|
|
||||||
...easJson,
|
|
||||||
cli: {
|
|
||||||
...easJson.cli,
|
|
||||||
appVersionSource: AppVersionSource.REMOTE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(BuildVersionSetView, () => {
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setting version for platform android', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
expect.objectContaining({
|
|
||||||
buildVersion: '1000',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('printing current remote version before prompting for a new one', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '100',
|
|
||||||
storeVersion: '1.0.0',
|
|
||||||
}));
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
expect(Log.log).toHaveBeenCalledWith(expect.stringMatching('configured with versionCode 100'));
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('printing info that there is no remote version configured', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
expect(Log.log).toHaveBeenCalledWith(
|
|
||||||
expect.stringMatching('does not have any versionCode configured')
|
|
||||||
);
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
|
||||||
await expect(cmd.run()).rejects.toThrowError('Aborting...');
|
|
||||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
|
|
||||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
|
||||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
import { Workflow } from '@expo/eas-build-job';
|
|
||||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getMockAppFragment,
|
|
||||||
getMockEasJson,
|
|
||||||
mockCommandContext,
|
|
||||||
mockProjectId,
|
|
||||||
mockTestCommand,
|
|
||||||
} from './utils';
|
|
||||||
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version';
|
|
||||||
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version';
|
|
||||||
import BuildVersionSyncView from '../../commands/build/version/sync';
|
|
||||||
import { Target } from '../../credentials/ios/types';
|
|
||||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
|
||||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
|
||||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
|
||||||
import { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
|
|
||||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
|
||||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
|
||||||
import * as prompts from '../../prompts';
|
|
||||||
|
|
||||||
jest.mock('../../build/android/version');
|
|
||||||
jest.mock('../../build/ios/version');
|
|
||||||
jest.mock('../../project/applicationIdentifier');
|
|
||||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
|
||||||
jest.mock('../../graphql/queries/AppQuery');
|
|
||||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
|
||||||
jest.mock('../../project/workflow');
|
|
||||||
jest.mock('../../project/android/gradleUtils');
|
|
||||||
jest.mock('../../project/ios/target');
|
|
||||||
jest.mock('../../project/ios/scheme');
|
|
||||||
jest.mock('fs');
|
|
||||||
jest.mock('../../log');
|
|
||||||
jest.mock('../../prompts');
|
|
||||||
jest.mock('../../utils/json');
|
|
||||||
|
|
||||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
|
||||||
return {
|
|
||||||
...easJson,
|
|
||||||
cli: {
|
|
||||||
...easJson.cli,
|
|
||||||
appVersionSource: AppVersionSource.REMOTE,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe(BuildVersionSyncView, () => {
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version for managed project on platform android', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '1000',
|
|
||||||
storeVersion: '0.0.1',
|
|
||||||
}));
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
|
||||||
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
|
|
||||||
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(syncAndroidAsync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version for bare project on platform android', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '1000',
|
|
||||||
storeVersion: '0.0.1',
|
|
||||||
}));
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.GENERIC);
|
|
||||||
jest.mocked(getAppBuildGradleAsync).mockImplementation(async () => ({
|
|
||||||
android: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
|
||||||
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
|
|
||||||
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'ANDROID',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(syncAndroidAsync).toHaveBeenCalled();
|
|
||||||
expect(updateAndroidNativeVersionsAsync).toHaveBeenCalledWith({
|
|
||||||
projectDir: ctx.projectDir,
|
|
||||||
versionCode: 1000,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version for managed project on platform ios', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '1000',
|
|
||||||
storeVersion: '0.0.1',
|
|
||||||
}));
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=ios'], ctx);
|
|
||||||
const syncIosAsync = jest.spyOn(cmd, 'syncIosAsync' as any);
|
|
||||||
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'IOS',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(syncIosAsync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version for bare project on platform ios', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
|
||||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
|
||||||
});
|
|
||||||
const fakeTarget: Target = {
|
|
||||||
targetName: 'testapp',
|
|
||||||
bundleIdentifier: 'eas.test.com',
|
|
||||||
entitlements: {},
|
|
||||||
};
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
|
||||||
buildVersion: '1000',
|
|
||||||
storeVersion: '0.0.1',
|
|
||||||
}));
|
|
||||||
jest.mocked(prompts.promptAsync).mockImplementation(async () => ({
|
|
||||||
version: '1000',
|
|
||||||
}));
|
|
||||||
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.GENERIC);
|
|
||||||
jest.mocked(resolveTargetsAsync).mockImplementation(async () => [fakeTarget]);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=ios'], ctx);
|
|
||||||
const syncIosAsync = jest.spyOn(cmd, 'syncIosAsync' as any);
|
|
||||||
|
|
||||||
await cmd.run();
|
|
||||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
|
||||||
ctx.loggedIn.graphqlClient,
|
|
||||||
mockProjectId,
|
|
||||||
'IOS',
|
|
||||||
'eas.test.com'
|
|
||||||
);
|
|
||||||
expect(syncIosAsync).toHaveBeenCalled();
|
|
||||||
expect(updateIosNativeVersionsAsync).toHaveBeenCalledWith({
|
|
||||||
buildNumber: '1000',
|
|
||||||
projectDir: ctx.projectDir,
|
|
||||||
targets: [fakeTarget],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
|
||||||
await expect(cmd.run()).rejects.toThrowError('Aborting...');
|
|
||||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
|
|
||||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
|
||||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
|
||||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
|
||||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
|
|
||||||
|
|
||||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
|
||||||
await cmd.run();
|
|
||||||
|
|
||||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
|
||||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { ExpoConfig } from '@expo/config-types';
|
|
||||||
import { EasJson } from '@expo/eas-json';
|
|
||||||
import { Command, Config } from '@oclif/core';
|
|
||||||
import { vol } from 'memfs';
|
|
||||||
import path from 'path';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
|
|
||||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
|
||||||
import { AppFragment, Role } from '../../graphql/generated';
|
|
||||||
|
|
||||||
export function getMockEasJson(): EasJson {
|
|
||||||
return {
|
|
||||||
cli: {},
|
|
||||||
build: {
|
|
||||||
production: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mockProjectId = 'fake-project-id';
|
|
||||||
|
|
||||||
export function getMockAppFragment(): AppFragment {
|
|
||||||
return {
|
|
||||||
id: mockProjectId,
|
|
||||||
slug: 'testapp',
|
|
||||||
name: 'testapp',
|
|
||||||
fullName: '@testuser/testpp',
|
|
||||||
ownerAccount: {
|
|
||||||
id: 'test-account-id',
|
|
||||||
name: 'testuser',
|
|
||||||
users: [
|
|
||||||
{
|
|
||||||
role: Role.Owner,
|
|
||||||
actor: {
|
|
||||||
id: 'test-user-id',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMockExpoConfig(): ExpoConfig {
|
|
||||||
return {
|
|
||||||
name: 'testapp',
|
|
||||||
slug: 'tetsapp',
|
|
||||||
extra: {
|
|
||||||
projectId: mockProjectId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LocalProjectContext {
|
|
||||||
projectDir: string;
|
|
||||||
exp?: ExpoConfig;
|
|
||||||
appId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockCommandContext<
|
|
||||||
C extends {
|
|
||||||
[name: string]: any;
|
|
||||||
} = object,
|
|
||||||
>(
|
|
||||||
commandClass: { contextDefinition: ContextInput<C> },
|
|
||||||
overrides: {
|
|
||||||
easJson?: EasJson;
|
|
||||||
exp?: ExpoConfig;
|
|
||||||
projectId?: string;
|
|
||||||
getDynamicPrivateProjectConfigAsync?: DynamicConfigContextFn;
|
|
||||||
getDynamicPublicProjectConfigAsync?: DynamicConfigContextFn;
|
|
||||||
}
|
|
||||||
): ContextOutput<C> {
|
|
||||||
const projectDir = path.join('/test', uuidv4());
|
|
||||||
vol.reset();
|
|
||||||
vol.fromJSON({ 'eas.json': JSON.stringify(overrides.easJson ?? getMockEasJson()) }, projectDir);
|
|
||||||
const contextDefinition = commandClass.contextDefinition;
|
|
||||||
|
|
||||||
const result: any = {};
|
|
||||||
for (const [contextKey] of Object.entries(contextDefinition)) {
|
|
||||||
if (contextKey === 'loggedIn') {
|
|
||||||
result.loggedIn = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contextKey === 'projectDir') {
|
|
||||||
result.projectDir = projectDir;
|
|
||||||
}
|
|
||||||
if (contextKey === 'maybeLoggedIn') {
|
|
||||||
result.maybeLoggedIn = {};
|
|
||||||
}
|
|
||||||
if (contextKey === 'sessionManager') {
|
|
||||||
result.sessionManager = {};
|
|
||||||
}
|
|
||||||
if (contextKey === 'projectConfig') {
|
|
||||||
result.projectConfig = {
|
|
||||||
exp: overrides.exp ?? getMockExpoConfig(),
|
|
||||||
projectId: overrides.projectId ?? mockProjectId,
|
|
||||||
projectDir,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (contextKey === 'getDynamicPrivateProjectConfigAsync') {
|
|
||||||
result.getDynamicPrivateProjectConfigAsync = () => ({
|
|
||||||
exp: overrides.exp ?? getMockExpoConfig(),
|
|
||||||
projectId: overrides.projectId ?? mockProjectId,
|
|
||||||
projectDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contextKey === 'getDynamicPublicProjectConfigAsync') {
|
|
||||||
result.getDynamicPublicProjectConfigAsync = () => ({
|
|
||||||
exp: overrides.exp ?? getMockExpoConfig(),
|
|
||||||
projectId: overrides.projectId ?? mockProjectId,
|
|
||||||
projectDir,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function mockTestCommand<T extends Command>(
|
|
||||||
commandConstructor: new (argv: string[], config: Config) => T,
|
|
||||||
argv: string[],
|
|
||||||
ctx: any
|
|
||||||
): T {
|
|
||||||
const cmd = new commandConstructor(argv, new Config({ root: __dirname }));
|
|
||||||
(cmd as any).getContextAsync = jest.fn(() => ctx);
|
|
||||||
return cmd;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NoErrorThrownError extends Error {}
|
|
||||||
|
|
||||||
export const getErrorAsync = async <TError = any>(
|
|
||||||
call: () => unknown
|
|
||||||
): Promise<TError | NoErrorThrownError> => {
|
|
||||||
try {
|
|
||||||
await call();
|
|
||||||
throw new NoErrorThrownError();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return error as TError;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getError = <TError = any>(call: () => unknown): TError | NoErrorThrownError => {
|
|
||||||
try {
|
|
||||||
call();
|
|
||||||
throw new NoErrorThrownError();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
return error as TError;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { fixS3Url } from '../uploads';
|
||||||
|
|
||||||
|
describe(fixS3Url, () => {
|
||||||
|
it('fixes broken links', () => {
|
||||||
|
const input =
|
||||||
|
'https://submission-service-archives.s3.amazonaws.com/production%2Fdc98ca84-1473-4cb3-ae81-8c7b291cb27e%2F4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||||
|
const expectedOutput =
|
||||||
|
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||||
|
expect(fixS3Url(input)).toBe(expectedOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not change correct urls', () => {
|
||||||
|
const correctUrl =
|
||||||
|
'https://submission-service-archives.s3.amazonaws.com/production/dc98ca84-1473-4cb3-ae81-8c7b291cb27e/4424aa95-b985-4e2f-8755-9507b1037c1c';
|
||||||
|
expect(fixS3Url(correctUrl)).toBe(correctUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import RudderAnalytics from '@expo/rudder-sdk-node';
|
|
||||||
import os from 'os';
|
|
||||||
import { URL } from 'url';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { Actor, getActorDisplayName } from '../user/User';
|
|
||||||
import UserSettings from '../user/UserSettings';
|
|
||||||
import { easCliVersion } from '../utils/easCli';
|
|
||||||
|
|
||||||
const PLATFORM_TO_ANALYTICS_PLATFORM: Partial<Record<NodeJS.Platform, string>> = {
|
|
||||||
darwin: 'Mac',
|
|
||||||
win32: 'Windows',
|
|
||||||
linux: 'Linux',
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AnalyticsEvent = CommandEvent | BuildEvent | SubmissionEvent | MetadataEvent;
|
|
||||||
|
|
||||||
export enum CommandEvent {
|
|
||||||
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum SubmissionEvent {
|
|
||||||
SUBMIT_COMMAND = 'submit cli submit command',
|
|
||||||
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
|
||||||
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
|
||||||
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
|
||||||
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
|
||||||
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
|
||||||
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
|
||||||
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
|
||||||
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
|
||||||
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
|
||||||
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
|
||||||
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
|
||||||
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
|
||||||
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
|
||||||
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
|
||||||
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum BuildEvent {
|
|
||||||
BUILD_COMMAND = 'build cli build command',
|
|
||||||
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
|
||||||
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
|
||||||
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
|
||||||
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
|
||||||
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
|
||||||
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
|
||||||
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
|
||||||
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
|
||||||
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
|
||||||
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
|
||||||
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
|
||||||
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
|
||||||
|
|
||||||
BUILD_STATUS_COMMAND = 'build cli build status',
|
|
||||||
|
|
||||||
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
|
||||||
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
|
||||||
|
|
||||||
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MetadataEvent {
|
|
||||||
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
|
||||||
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AnalyticsEventProperties = Record<string, string | number | boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interface for commands to use to log events to analytics.
|
|
||||||
*/
|
|
||||||
export interface Analytics {
|
|
||||||
logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The interface for commands to use to orchestrate the analytics system. Should only be necessary to use
|
|
||||||
* this within EASCommand.
|
|
||||||
*/
|
|
||||||
export interface AnalyticsWithOrchestration extends Analytics {
|
|
||||||
setActor(actor: Actor): void;
|
|
||||||
flushAsync(): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const USER_SETTINGS_KEY_AMPLITUDE_ENABLED = 'amplitudeEnabled';
|
|
||||||
const USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID = 'amplitudeDeviceId';
|
|
||||||
const USER_SETTINGS_KEY_ANALYTICS_ENABLED = 'analyticsEnabled';
|
|
||||||
const USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID = 'analyticsDeviceId';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the user's analytics enabled preference. Note that this will only take effect
|
|
||||||
* upon the next run of the CLI.
|
|
||||||
*/
|
|
||||||
export async function setAnalyticsEnabledAsync(enabled: boolean): Promise<void> {
|
|
||||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the user's analytics enabled preference.
|
|
||||||
*/
|
|
||||||
export async function getAnalyticsEnabledAsync(): Promise<boolean> {
|
|
||||||
const analyticsEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, null);
|
|
||||||
return !!analyticsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an instance of Analytics based on the user's analytics enabled preferences.
|
|
||||||
*/
|
|
||||||
export async function createAnalyticsAsync(): Promise<AnalyticsWithOrchestration> {
|
|
||||||
// TODO: remove after some time
|
|
||||||
const amplitudeEnabled = await UserSettings.getAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED, null);
|
|
||||||
if (amplitudeEnabled !== null) {
|
|
||||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, amplitudeEnabled);
|
|
||||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_ENABLED);
|
|
||||||
}
|
|
||||||
const amplitudeDeviceId = await UserSettings.getAsync(
|
|
||||||
USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
if (amplitudeDeviceId !== null) {
|
|
||||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, amplitudeDeviceId);
|
|
||||||
await UserSettings.deleteKeyAsync(USER_SETTINGS_KEY_AMPLITUDE_DEVICE_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.DISABLE_EAS_ANALYTICS) {
|
|
||||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_ENABLED, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
const persistedDeviceId = await UserSettings.getAsync(
|
|
||||||
USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID,
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const deviceId = persistedDeviceId ?? uuidv4();
|
|
||||||
if (!persistedDeviceId) {
|
|
||||||
await UserSettings.setAsync(USER_SETTINGS_KEY_ANALYTICS_DEVICE_ID, deviceId);
|
|
||||||
}
|
|
||||||
return new RudderstackAnalytics(deviceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NoOpAnalytics implements AnalyticsWithOrchestration {
|
|
||||||
logEvent(): void {}
|
|
||||||
setActor(): void {}
|
|
||||||
async flushAsync(): Promise<void> {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const RudderstackAnalyticsConfig =
|
|
||||||
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
|
||||||
? {
|
|
||||||
// staging environment
|
|
||||||
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
|
||||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
// prod environment
|
|
||||||
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
|
||||||
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
|
||||||
};
|
|
||||||
|
|
||||||
class RudderstackAnalytics implements AnalyticsWithOrchestration {
|
|
||||||
private readonly rudderstackClient = new RudderAnalytics(
|
|
||||||
RudderstackAnalyticsConfig.rudderstackWriteKey,
|
|
||||||
new URL('/v1/batch', RudderstackAnalyticsConfig.rudderstackDataPlaneURL).toString(),
|
|
||||||
{
|
|
||||||
flushInterval: 300,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
private identifiedActor: Actor | null = null;
|
|
||||||
|
|
||||||
constructor(private readonly persistentDeviceId: string) {
|
|
||||||
// identify once with just anonymous ID. Once the actor is fetched, re-indentify with
|
|
||||||
// both so that they can be associated
|
|
||||||
this.rudderstackClient.identify({
|
|
||||||
anonymousId: persistentDeviceId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setActor(actor: Actor): void {
|
|
||||||
if (this.identifiedActor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.rudderstackClient.identify({
|
|
||||||
userId: actor.id,
|
|
||||||
anonymousId: this.persistentDeviceId,
|
|
||||||
traits: {
|
|
||||||
username: getActorDisplayName(actor),
|
|
||||||
user_id: actor.id,
|
|
||||||
user_type: actor.__typename,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.identifiedActor = actor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public logEvent(name: AnalyticsEvent, properties: AnalyticsEventProperties): void {
|
|
||||||
const userId = this.identifiedActor?.id;
|
|
||||||
const deviceId = this.persistentDeviceId;
|
|
||||||
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
|
||||||
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
|
||||||
this.rudderstackClient.track({
|
|
||||||
event: name,
|
|
||||||
properties: { ...properties, ...commonEventProperties },
|
|
||||||
...identity,
|
|
||||||
context: this.getRudderStackContext(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async flushAsync(): Promise<void> {
|
|
||||||
await this.rudderstackClient.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private getRudderStackContext(): Record<string, any> {
|
|
||||||
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
|
||||||
return {
|
|
||||||
os: { name: platform, version: os.release() },
|
|
||||||
device: { type: platform, model: platform },
|
|
||||||
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,23 @@
|
|||||||
import { Analytics, AnalyticsEvent, AnalyticsEventProperties } from './AnalyticsManager';
|
import { Analytics, Event } from './events';
|
||||||
|
|
||||||
|
export type TrackingContext = Record<string, string | number | boolean>;
|
||||||
|
|
||||||
export async function withAnalyticsAsync<Result>(
|
export async function withAnalyticsAsync<Result>(
|
||||||
analytics: Analytics,
|
|
||||||
fn: () => Promise<Result>,
|
fn: () => Promise<Result>,
|
||||||
{
|
analytics: {
|
||||||
attemptEvent,
|
attemptEvent: Event;
|
||||||
successEvent,
|
successEvent: Event;
|
||||||
failureEvent,
|
failureEvent: Event;
|
||||||
properties,
|
trackingCtx: TrackingContext;
|
||||||
}: {
|
|
||||||
attemptEvent: AnalyticsEvent;
|
|
||||||
successEvent: AnalyticsEvent;
|
|
||||||
failureEvent: AnalyticsEvent;
|
|
||||||
properties: AnalyticsEventProperties;
|
|
||||||
}
|
}
|
||||||
): Promise<Result> {
|
): Promise<Result> {
|
||||||
try {
|
try {
|
||||||
analytics.logEvent(attemptEvent, properties);
|
|
||||||
const result = await fn();
|
const result = await fn();
|
||||||
analytics.logEvent(successEvent, properties);
|
Analytics.logEvent(analytics.successEvent, analytics.trackingCtx);
|
||||||
return result;
|
return result;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
analytics.logEvent(failureEvent, {
|
Analytics.logEvent(analytics.failureEvent, {
|
||||||
...properties,
|
...analytics.trackingCtx,
|
||||||
reason: error.message,
|
reason: error.message,
|
||||||
});
|
});
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { logEvent } from './rudderstackClient';
|
||||||
|
export type Event = BuildEvent | SubmissionEvent | MetadataEvent;
|
||||||
|
|
||||||
|
export enum SubmissionEvent {
|
||||||
|
SUBMIT_COMMAND = 'submit cli submit command',
|
||||||
|
SUBMIT_COMMAND_ATTEMPT = 'submit cli attempt',
|
||||||
|
SUBMIT_COMMAND_SUCCESS = 'submit cli success',
|
||||||
|
SUBMIT_COMMAND_FAIL = 'submit cli fail',
|
||||||
|
GATHER_CREDENTIALS_ATTEMPT = 'submit cli gather credentials attempt',
|
||||||
|
GATHER_CREDENTIALS_SUCCESS = 'submit cli gather credentials success',
|
||||||
|
GATHER_CREDENTIALS_FAIL = 'submit cli gather credentials fail',
|
||||||
|
GATHER_ARCHIVE_ATTEMPT = 'submit cli gather archive attempt',
|
||||||
|
GATHER_ARCHIVE_SUCCESS = 'submit cli gather archive success',
|
||||||
|
GATHER_ARCHIVE_FAIL = 'submit cli gather archive fail',
|
||||||
|
SUBMIT_REQUEST_ATTEMPT = 'submit cli request attempt',
|
||||||
|
SUBMIT_REQUEST_SUCCESS = 'submit cli request success',
|
||||||
|
SUBMIT_REQUEST_FAIL = 'submit cli request fail',
|
||||||
|
API_KEY_DOWNLOAD_FAIL = 'submit cli credentials api key download fail',
|
||||||
|
API_KEY_DOWNLOAD_RETRY = 'submit cli credentials api key download fail temporary',
|
||||||
|
API_KEY_DOWNLOAD_SUCCESS = 'submit cli credentials api key download succeed',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BuildEvent {
|
||||||
|
BUILD_COMMAND = 'build cli build command',
|
||||||
|
PROJECT_UPLOAD_ATTEMPT = 'build cli project upload attempt',
|
||||||
|
PROJECT_UPLOAD_SUCCESS = 'build cli project upload success',
|
||||||
|
PROJECT_UPLOAD_FAIL = 'build cli project upload fail',
|
||||||
|
GATHER_CREDENTIALS_ATTEMPT = 'build cli gather credentials attempt',
|
||||||
|
GATHER_CREDENTIALS_SUCCESS = 'build cli gather credentials success',
|
||||||
|
GATHER_CREDENTIALS_FAIL = 'build cli gather credentials fail',
|
||||||
|
CONFIGURE_PROJECT_ATTEMPT = 'build cli configure project attempt',
|
||||||
|
CONFIGURE_PROJECT_SUCCESS = 'build cli configure project success',
|
||||||
|
CONFIGURE_PROJECT_FAIL = 'build cli configure project fail',
|
||||||
|
BUILD_REQUEST_ATTEMPT = 'build cli build request attempt',
|
||||||
|
BUILD_REQUEST_SUCCESS = 'build cli build request success',
|
||||||
|
BUILD_REQUEST_FAIL = 'build cli build request fail',
|
||||||
|
|
||||||
|
BUILD_STATUS_COMMAND = 'build cli build status',
|
||||||
|
|
||||||
|
CREDENTIALS_SYNC_COMMAND = 'build cli credentials sync command',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_LOCAL_ATTEMPT = 'build cli credentials sync update local attempt',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_LOCAL_SUCCESS = 'build cli credentials sync update local success',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_LOCAL_FAIL = 'build cli credentials sync update local fail',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_REMOTE_ATTEMPT = 'build cli credentials sync update remote attempt',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_REMOTE_SUCCESS = 'build cli credentials sync update remote success',
|
||||||
|
CREDENTIALS_SYNC_UPDATE_REMOTE_FAIL = 'build cli credentials sync update remote fail',
|
||||||
|
|
||||||
|
ANDROID_KEYSTORE_CREATE = 'build cli credentials keystore create',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetadataEvent {
|
||||||
|
APPLE_METADATA_DOWNLOAD = 'metadata cli download apple response',
|
||||||
|
APPLE_METADATA_UPLOAD = 'metadata cli upload apple response',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Analytics {
|
||||||
|
static logEvent(name: Event, properties: Record<string, any>): void {
|
||||||
|
logEvent(name, properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import RudderAnalytics from '@expo/rudder-sdk-node';
|
||||||
|
import os from 'os';
|
||||||
|
import { URL } from 'url';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import UserSettings from '../user/UserSettings';
|
||||||
|
import { easCliVersion } from '../utils/easCli';
|
||||||
|
|
||||||
|
const PLATFORM_TO_ANALYTICS_PLATFORM: { [platform: string]: string } = {
|
||||||
|
darwin: 'Mac',
|
||||||
|
win32: 'Windows',
|
||||||
|
linux: 'Linux',
|
||||||
|
};
|
||||||
|
|
||||||
|
let rudderstackClient: RudderAnalytics | null = null;
|
||||||
|
let userIdentified = false;
|
||||||
|
let identifyData: {
|
||||||
|
userId: string;
|
||||||
|
deviceId: string;
|
||||||
|
traits: Record<string, any>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
export async function initAsync(): Promise<void> {
|
||||||
|
// TODO: remove after some time
|
||||||
|
const amplitudeEnabled = await UserSettings.getAsync('amplitudeEnabled', null);
|
||||||
|
if (amplitudeEnabled !== null) {
|
||||||
|
await UserSettings.setAsync('analyticsEnabled', amplitudeEnabled);
|
||||||
|
await UserSettings.deleteKeyAsync('amplitudeEnabled');
|
||||||
|
}
|
||||||
|
const amplitudeDeviceId = await UserSettings.getAsync('amplitudeDeviceId', null);
|
||||||
|
if (amplitudeDeviceId !== null) {
|
||||||
|
await UserSettings.setAsync('analyticsDeviceId', amplitudeDeviceId);
|
||||||
|
await UserSettings.deleteKeyAsync('amplitudeDeviceId');
|
||||||
|
}
|
||||||
|
// TODO: cut here
|
||||||
|
if (process.env.DISABLE_EAS_ANALYTICS) {
|
||||||
|
await UserSettings.setAsync('analyticsEnabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analyticsEnabled = await UserSettings.getAsync('analyticsEnabled', true);
|
||||||
|
if (analyticsEnabled) {
|
||||||
|
const config =
|
||||||
|
process.env.EXPO_STAGING || process.env.EXPO_LOCAL
|
||||||
|
? {
|
||||||
|
// staging environment
|
||||||
|
rudderstackWriteKey: '1wpX20Da4ltFGSXbPFYUL00Chb7',
|
||||||
|
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
// prod environment
|
||||||
|
rudderstackWriteKey: '1wpXLFxmujq86etH6G6cc90hPcC',
|
||||||
|
rudderstackDataPlaneURL: 'https://cdp.expo.dev',
|
||||||
|
};
|
||||||
|
|
||||||
|
rudderstackClient = new RudderAnalytics(
|
||||||
|
config.rudderstackWriteKey,
|
||||||
|
new URL('/v1/batch', config.rudderstackDataPlaneURL).toString(),
|
||||||
|
{
|
||||||
|
flushInterval: 300,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setUserDataAsync(userId: string, traits: Record<string, any>): Promise<void> {
|
||||||
|
const savedDeviceId = await UserSettings.getAsync('analyticsDeviceId', null);
|
||||||
|
const deviceId = savedDeviceId ?? uuidv4();
|
||||||
|
if (!savedDeviceId) {
|
||||||
|
await UserSettings.setAsync('analyticsDeviceId', deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
identifyData = {
|
||||||
|
userId,
|
||||||
|
deviceId,
|
||||||
|
traits,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureUserIdentified();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function flushAsync(): Promise<void> {
|
||||||
|
if (rudderstackClient) {
|
||||||
|
await rudderstackClient.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logEvent(name: string, properties: Record<string, any> = {}): void {
|
||||||
|
if (!rudderstackClient) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureUserIdentified();
|
||||||
|
|
||||||
|
const { userId, deviceId } = identifyData ?? {};
|
||||||
|
const commonEventProperties = { source_version: easCliVersion, source: 'eas cli' };
|
||||||
|
|
||||||
|
const identity = { userId: userId ?? undefined, anonymousId: deviceId ?? uuidv4() };
|
||||||
|
rudderstackClient.track({
|
||||||
|
event: name,
|
||||||
|
properties: { ...properties, ...commonEventProperties },
|
||||||
|
...identity,
|
||||||
|
context: getRudderStackContext(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureUserIdentified(): void {
|
||||||
|
if (!rudderstackClient || userIdentified || !identifyData) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rudderstackClient.identify({
|
||||||
|
userId: identifyData.userId,
|
||||||
|
anonymousId: identifyData.deviceId,
|
||||||
|
traits: identifyData.traits,
|
||||||
|
});
|
||||||
|
userIdentified = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRudderStackContext(): Record<string, any> {
|
||||||
|
const platform = PLATFORM_TO_ANALYTICS_PLATFORM[os.platform()] || os.platform();
|
||||||
|
return {
|
||||||
|
os: { name: platform, version: os.release() },
|
||||||
|
device: { type: platform, model: platform },
|
||||||
|
app: { name: 'eas cli', version: easCliVersion ?? undefined },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AnalyticsEvent {
|
||||||
|
ACTION = 'action', // generic event type which is used to determine the 'daily active user' stat, include an `action: eas ${subcommand}` property inside of the event properties object
|
||||||
|
}
|
||||||
+74
-59
@@ -1,78 +1,93 @@
|
|||||||
import { JSONValue } from '@expo/json-file';
|
import { JSONValue } from '@expo/json-file';
|
||||||
|
|
||||||
import { ApiV2Error } from './ApiV2Error';
|
|
||||||
import fetch, { RequestError, RequestInit } from './fetch';
|
import fetch, { RequestError, RequestInit } from './fetch';
|
||||||
|
import { getAccessToken, getSessionSecret } from './user/sessionStorage';
|
||||||
|
|
||||||
|
export class ApiV2Error extends Error {
|
||||||
|
override readonly name = 'ApiV2Error';
|
||||||
|
readonly expoApiV2ErrorCode: string;
|
||||||
|
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||||
|
readonly expoApiV2ErrorServerStack?: string;
|
||||||
|
readonly expoApiV2ErrorMetadata?: object;
|
||||||
|
|
||||||
|
constructor(response: {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
stack?: string;
|
||||||
|
details?: JSONValue;
|
||||||
|
metadata?: object;
|
||||||
|
}) {
|
||||||
|
super(response.message);
|
||||||
|
this.expoApiV2ErrorCode = response.code;
|
||||||
|
this.expoApiV2ErrorDetails = response.details;
|
||||||
|
this.expoApiV2ErrorServerStack = response.stack;
|
||||||
|
this.expoApiV2ErrorMetadata = response.metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
interface RequestOptions {
|
interface RequestOptions {
|
||||||
body: JSONValue;
|
body: JSONValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ApiV2Client {
|
export const api = {
|
||||||
constructor(
|
async putAsync(path: string, options: RequestOptions): Promise<any> {
|
||||||
private readonly authInfo: {
|
return await requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
|
||||||
accessToken: string | null;
|
},
|
||||||
sessionSecret: string | null;
|
|
||||||
}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public async putAsync(path: string, options: RequestOptions): Promise<any> {
|
async postAsync(path: string, options: RequestOptions): Promise<any> {
|
||||||
return await this.requestAsync(path, { method: 'PUT', body: JSON.stringify(options.body) });
|
return await requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAsync(path: string): Promise<any> {
|
||||||
|
return await requestAsync(path, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
|
||||||
|
async getAsync(path: string): Promise<any> {
|
||||||
|
return await requestAsync(path, { method: 'GET' });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function requestAsync(path: string, options: RequestInit): Promise<any> {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...getAuthHeaders(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (err) {
|
||||||
|
await handleApiErrorAsync(err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async postAsync(path: string, options: RequestOptions): Promise<any> {
|
async function handleApiErrorAsync(err: any): Promise<void> {
|
||||||
return await this.requestAsync(path, { method: 'POST', body: JSON.stringify(options.body) });
|
if (err instanceof RequestError) {
|
||||||
}
|
let result: { [key: string]: any };
|
||||||
|
|
||||||
public async deleteAsync(path: string): Promise<any> {
|
|
||||||
return await this.requestAsync(path, { method: 'DELETE' });
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getAsync(path: string): Promise<any> {
|
|
||||||
return await this.requestAsync(path, { method: 'GET' });
|
|
||||||
}
|
|
||||||
|
|
||||||
private getAuthHeaders(): Record<string, string> {
|
|
||||||
const token = this.authInfo.accessToken;
|
|
||||||
if (token) {
|
|
||||||
return { authorization: `Bearer ${token}` };
|
|
||||||
}
|
|
||||||
const sessionSecret = this.authInfo.sessionSecret;
|
|
||||||
if (sessionSecret) {
|
|
||||||
return { 'expo-session': sessionSecret };
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async requestAsync(path: string, options: RequestInit): Promise<any> {
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${getExpoApiBaseUrl()}/v2/${path}`, {
|
result = (await err.response.json()) as { [key: string]: any };
|
||||||
...options,
|
} catch {
|
||||||
headers: {
|
throw new Error(`Malformed api response: ${await err.response.text()}`);
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...this.getAuthHeaders(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
} catch (err) {
|
|
||||||
await ApiV2Client.handleApiErrorAsync(err);
|
|
||||||
}
|
}
|
||||||
|
if (result.errors?.length) {
|
||||||
|
throw new ApiV2Error(result.errors[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static async handleApiErrorAsync(err: any): Promise<void> {
|
function getAuthHeaders(): Record<string, string> {
|
||||||
if (err instanceof RequestError) {
|
const token = getAccessToken();
|
||||||
let result: { [key: string]: any };
|
if (token) {
|
||||||
try {
|
return { authorization: `Bearer ${token}` };
|
||||||
result = (await err.response.json()) as { [key: string]: any };
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Malformed api response: ${await err.response.text()}`);
|
|
||||||
}
|
|
||||||
if (result.errors?.length) {
|
|
||||||
throw new ApiV2Error(result.errors[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const sessionSecret = getSessionSecret();
|
||||||
|
if (sessionSecret) {
|
||||||
|
return { 'expo-session': sessionSecret };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExpoApiBaseUrl(): string {
|
export function getExpoApiBaseUrl(): string {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
|
|
||||||
|
|
||||||
export const testBasicBranchInfo: UpdateBranchBasicInfoFragment = {
|
|
||||||
id: 'c4fec95d-7ab1-4d35-bfe6-fe7ff05beda9',
|
|
||||||
name: 'development',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
};
|
|
||||||
export const testBasicBranchInfo2: UpdateBranchBasicInfoFragment = {
|
|
||||||
id: '43ccb8a4-8aaa-47f0-9790-dcc80933441b',
|
|
||||||
name: 'preview',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { EASUpdateAction, EASUpdateContext, NonInteractiveError } from '../../eas-update/utils';
|
|
||||||
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
|
|
||||||
import { BranchQuery } from '../../graphql/queries/BranchQuery';
|
|
||||||
import {
|
|
||||||
Connection,
|
|
||||||
Edge,
|
|
||||||
FilterPagination,
|
|
||||||
QueryParams,
|
|
||||||
selectPaginatedAsync,
|
|
||||||
} from '../../utils/relay';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a branch for the project.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {function} options.filterPredicate - A predicate to filter the branches that are shown to the user. It takes a branchInfo object as a parameter and returns a boolean.
|
|
||||||
* @param {string} options.printedType - The type of branch printed to the user. Defaults to 'branch'.
|
|
||||||
* @param {number} options.pageSize - The number of branches to show per page. Defaults to 100.
|
|
||||||
* @param {function} options.beforeEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called before each query.
|
|
||||||
* @args externalQueryParams The query params for the pagination.
|
|
||||||
* @args totalNodesFetched The total number of nodes fetched so far.
|
|
||||||
* @args dataset The dataset so far.
|
|
||||||
* @param {function} options.afterEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called after each query.
|
|
||||||
* @args externalQueryParams The query params for the pagination.
|
|
||||||
* @args totalNodesFetched The total number of nodes fetched so far.
|
|
||||||
* @args dataset The dataset so far.
|
|
||||||
* @args willFetchAgain If the query will fetch again to get a complete page.
|
|
||||||
*/
|
|
||||||
export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> {
|
|
||||||
constructor(
|
|
||||||
private options: {
|
|
||||||
filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean;
|
|
||||||
printedType?: string;
|
|
||||||
pageSize?: number;
|
|
||||||
beforeEachFilterQuery?: (
|
|
||||||
externalQueryParams: QueryParams,
|
|
||||||
totalNodesFetched: number,
|
|
||||||
dataset: Edge<UpdateBranchBasicInfoFragment>[]
|
|
||||||
) => void;
|
|
||||||
afterEachFilterQuery?: (
|
|
||||||
externalQueryParams: QueryParams,
|
|
||||||
totalNodesFetched: number,
|
|
||||||
dataset: Edge<UpdateBranchBasicInfoFragment>[],
|
|
||||||
willFetchAgain: boolean
|
|
||||||
) => void;
|
|
||||||
} = {}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async queryAsync(
|
|
||||||
ctx: EASUpdateContext,
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
|
|
||||||
const { graphqlClient, app } = ctx;
|
|
||||||
const { projectId } = app;
|
|
||||||
return await BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync(graphqlClient, {
|
|
||||||
appId: projectId,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterQueryAsync(
|
|
||||||
ctx: EASUpdateContext,
|
|
||||||
queryParams: QueryParams,
|
|
||||||
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) => boolean
|
|
||||||
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
|
|
||||||
const queryAsync = async (
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
|
|
||||||
await this.queryAsync(ctx, queryParams);
|
|
||||||
return await FilterPagination.getPageAsync({
|
|
||||||
queryParams,
|
|
||||||
queryAsync,
|
|
||||||
filterPredicate,
|
|
||||||
beforeEachQuery: this.options.beforeEachFilterQuery,
|
|
||||||
afterEachQuery: this.options.afterEachFilterQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runAsync(ctx: EASUpdateContext): Promise<UpdateBranchBasicInfoFragment | null> {
|
|
||||||
const { nonInteractive } = ctx;
|
|
||||||
const { filterPredicate } = this.options;
|
|
||||||
const printedType = this.options.printedType ?? 'branch';
|
|
||||||
const pageSize = this.options.pageSize ?? 100;
|
|
||||||
if (nonInteractive) {
|
|
||||||
throw new NonInteractiveError(
|
|
||||||
`${printedType} selection cannot be run in non-interactive mode.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryAsync = async (
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
|
|
||||||
filterPredicate
|
|
||||||
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
|
||||||
: this.queryAsync(ctx, queryParams);
|
|
||||||
|
|
||||||
const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> =>
|
|
||||||
branchInfo.name;
|
|
||||||
|
|
||||||
return await selectPaginatedAsync({
|
|
||||||
queryAsync,
|
|
||||||
getTitleAsync,
|
|
||||||
printedType,
|
|
||||||
pageSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
|
|
||||||
import { NonInteractiveError } from '../../../eas-update/utils';
|
|
||||||
import { UpdateBranchBasicInfoFragment } from '../../../graphql/generated';
|
|
||||||
import { BranchQuery } from '../../../graphql/queries/BranchQuery';
|
|
||||||
import { promptAsync } from '../../../prompts';
|
|
||||||
import { testBasicBranchInfo, testBasicBranchInfo2 } from '../../__tests__/fixtures';
|
|
||||||
import { SelectBranch } from '../SelectBranch';
|
|
||||||
|
|
||||||
jest.mock('../../../prompts');
|
|
||||||
jest.mock('../../../graphql/queries/BranchQuery');
|
|
||||||
describe(SelectBranch, () => {
|
|
||||||
afterEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
});
|
|
||||||
it('returns null if project has no branchs', async () => {
|
|
||||||
const ctx = createCtxMock();
|
|
||||||
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
|
|
||||||
edges: [],
|
|
||||||
pageInfo: {
|
|
||||||
hasPreviousPage: false,
|
|
||||||
hasNextPage: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const selectBranch = new SelectBranch();
|
|
||||||
const branch = await selectBranch.runAsync(ctx);
|
|
||||||
expect(branch).toBeNull();
|
|
||||||
});
|
|
||||||
it('still prompts if project has only one branch', async () => {
|
|
||||||
const ctx = createCtxMock();
|
|
||||||
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
|
|
||||||
edges: [{ node: testBasicBranchInfo, cursor: 'cursor' }],
|
|
||||||
pageInfo: {
|
|
||||||
hasPreviousPage: false,
|
|
||||||
hasNextPage: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
|
||||||
item: testBasicBranchInfo,
|
|
||||||
}));
|
|
||||||
const selectBranch = new SelectBranch();
|
|
||||||
const branch = await selectBranch.runAsync(ctx);
|
|
||||||
expect(branch).toBe(testBasicBranchInfo);
|
|
||||||
expect(promptAsync).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
it('returns the selected branch', async () => {
|
|
||||||
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
|
|
||||||
edges: [
|
|
||||||
{ node: testBasicBranchInfo, cursor: 'cursor' },
|
|
||||||
{ node: testBasicBranchInfo2, cursor: 'cursor' },
|
|
||||||
],
|
|
||||||
pageInfo: {
|
|
||||||
hasPreviousPage: false,
|
|
||||||
hasNextPage: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
jest.mocked(promptAsync).mockImplementation(async () => ({
|
|
||||||
item: testBasicBranchInfo,
|
|
||||||
}));
|
|
||||||
const ctx = createCtxMock();
|
|
||||||
const selectBranch = new SelectBranch();
|
|
||||||
const branch = await selectBranch.runAsync(ctx);
|
|
||||||
expect(branch).toBe(testBasicBranchInfo);
|
|
||||||
});
|
|
||||||
it('returns the only available branch with a filter', async () => {
|
|
||||||
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
|
|
||||||
edges: [
|
|
||||||
{ node: testBasicBranchInfo, cursor: 'cursor' },
|
|
||||||
{ node: testBasicBranchInfo2, cursor: 'cursor' },
|
|
||||||
],
|
|
||||||
pageInfo: {
|
|
||||||
hasPreviousPage: false,
|
|
||||||
hasNextPage: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const ctx = createCtxMock();
|
|
||||||
const selectBranch = new SelectBranch({
|
|
||||||
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) =>
|
|
||||||
branchInfo === testBasicBranchInfo,
|
|
||||||
});
|
|
||||||
const branch = await selectBranch.runAsync(ctx);
|
|
||||||
expect(branch).toBe(testBasicBranchInfo);
|
|
||||||
});
|
|
||||||
it('throws an error in Non-Interactive Mode', async () => {
|
|
||||||
const ctx = createCtxMock({
|
|
||||||
nonInteractive: true,
|
|
||||||
});
|
|
||||||
const selectBranch = new SelectBranch();
|
|
||||||
|
|
||||||
// dont fail if users are running in non-interactive mode
|
|
||||||
await expect(selectBranch.runAsync(ctx)).rejects.toThrowError(NonInteractiveError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,42 +1,30 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import gql from 'graphql-tag';
|
import CliTable from 'cli-table3';
|
||||||
|
|
||||||
import { BranchNotFoundError } from './utils';
|
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||||
import { withErrorHandlingAsync } from '../graphql/client';
|
import { UpdateBranchFragment } from '../graphql/generated';
|
||||||
import {
|
|
||||||
CreateUpdateBranchForAppMutation,
|
|
||||||
CreateUpdateBranchForAppMutationVariables,
|
|
||||||
UpdateBranch,
|
|
||||||
UpdateBranchFragment,
|
|
||||||
} from '../graphql/generated';
|
|
||||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||||
import Log from '../log';
|
import Log from '../log';
|
||||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
import { UPDATE_COLUMNS, formatUpdateMessage, getPlatformsForGroup } from '../update/utils';
|
||||||
import { printJsonOnlyOutput } from '../utils/json';
|
import { printJsonOnlyOutput } from '../utils/json';
|
||||||
import {
|
import {
|
||||||
SelectPromptEntry,
|
|
||||||
paginatedQueryWithConfirmPromptAsync,
|
paginatedQueryWithConfirmPromptAsync,
|
||||||
paginatedQueryWithSelectPromptAsync,
|
paginatedQueryWithSelectPromptAsync,
|
||||||
} from '../utils/queries';
|
} from '../utils/queries';
|
||||||
|
|
||||||
export const BRANCHES_LIMIT = 50;
|
export const BRANCHES_LIMIT = 50;
|
||||||
|
|
||||||
export async function selectBranchOnAppAsync(
|
export async function selectBranchOnAppAsync({
|
||||||
graphqlClient: ExpoGraphqlClient,
|
projectId,
|
||||||
{
|
promptTitle,
|
||||||
projectId,
|
displayTextForListItem,
|
||||||
promptTitle,
|
paginatedQueryOptions,
|
||||||
displayTextForListItem,
|
}: {
|
||||||
paginatedQueryOptions,
|
projectId: string;
|
||||||
}: {
|
displayTextForListItem: (queryItem: UpdateBranchFragment) => string;
|
||||||
projectId: string;
|
promptTitle: string;
|
||||||
displayTextForListItem: (queryItem: UpdateBranchFragment) => SelectPromptEntry;
|
paginatedQueryOptions: PaginatedQueryOptions;
|
||||||
promptTitle: string;
|
}): Promise<UpdateBranchFragment> {
|
||||||
paginatedQueryOptions: PaginatedQueryOptions;
|
|
||||||
}
|
|
||||||
): Promise<UpdateBranchFragment> {
|
|
||||||
if (paginatedQueryOptions.nonInteractive) {
|
if (paginatedQueryOptions.nonInteractive) {
|
||||||
throw new Error('Unable to select a branch in non-interactive mode.');
|
throw new Error('Unable to select a branch in non-interactive mode.');
|
||||||
}
|
}
|
||||||
@@ -44,12 +32,11 @@ export async function selectBranchOnAppAsync(
|
|||||||
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
|
const selectedBranch = await paginatedQueryWithSelectPromptAsync({
|
||||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||||
offset: paginatedQueryOptions.offset,
|
offset: paginatedQueryOptions.offset,
|
||||||
queryToPerform: (limit, offset) =>
|
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
|
||||||
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
|
|
||||||
promptOptions: {
|
promptOptions: {
|
||||||
title: promptTitle,
|
title: promptTitle,
|
||||||
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
|
getIdentifierForQueryItem: updateBranchFragment => updateBranchFragment.id,
|
||||||
makePartialChoiceObject: displayTextForListItem,
|
createDisplayTextForSelectionPromptListItem: displayTextForListItem,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!selectedBranch) {
|
if (!selectedBranch) {
|
||||||
@@ -58,19 +45,15 @@ export async function selectBranchOnAppAsync(
|
|||||||
return selectedBranch;
|
return selectedBranch;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAndRenderBranchesOnAppAsync(
|
export async function listAndRenderBranchesOnAppAsync({
|
||||||
graphqlClient: ExpoGraphqlClient,
|
projectId,
|
||||||
{
|
paginatedQueryOptions,
|
||||||
projectId,
|
}: {
|
||||||
paginatedQueryOptions,
|
projectId: string;
|
||||||
}: {
|
paginatedQueryOptions: PaginatedQueryOptions;
|
||||||
projectId: string;
|
}): Promise<void> {
|
||||||
paginatedQueryOptions: PaginatedQueryOptions;
|
|
||||||
}
|
|
||||||
): Promise<void> {
|
|
||||||
if (paginatedQueryOptions.nonInteractive) {
|
if (paginatedQueryOptions.nonInteractive) {
|
||||||
const branches = await queryBranchesOnProjectAsync(
|
const branches = await queryBranchesOnProjectAsync(
|
||||||
graphqlClient,
|
|
||||||
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||||
paginatedQueryOptions.offset,
|
paginatedQueryOptions.offset,
|
||||||
projectId
|
projectId
|
||||||
@@ -80,8 +63,7 @@ export async function listAndRenderBranchesOnAppAsync(
|
|||||||
await paginatedQueryWithConfirmPromptAsync({
|
await paginatedQueryWithConfirmPromptAsync({
|
||||||
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
limit: paginatedQueryOptions.limit ?? BRANCHES_LIMIT,
|
||||||
offset: paginatedQueryOptions.offset,
|
offset: paginatedQueryOptions.offset,
|
||||||
queryToPerform: (limit, offset) =>
|
queryToPerform: (limit, offset) => queryBranchesOnProjectAsync(limit, offset, projectId),
|
||||||
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
|
|
||||||
promptOptions: {
|
promptOptions: {
|
||||||
title: 'Load more branches?',
|
title: 'Load more branches?',
|
||||||
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
|
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
|
||||||
@@ -91,12 +73,11 @@ export async function listAndRenderBranchesOnAppAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function queryBranchesOnProjectAsync(
|
async function queryBranchesOnProjectAsync(
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
limit: number,
|
limit: number,
|
||||||
offset: number,
|
offset: number,
|
||||||
projectId: string
|
projectId: string
|
||||||
): Promise<UpdateBranchFragment[]> {
|
): Promise<UpdateBranchFragment[]> {
|
||||||
return await BranchQuery.listBranchesOnAppAsync(graphqlClient, {
|
return await BranchQuery.listBranchesOnAppAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@@ -110,75 +91,34 @@ function renderPageOfBranches(
|
|||||||
if (json) {
|
if (json) {
|
||||||
printJsonOnlyOutput(currentPage);
|
printJsonOnlyOutput(currentPage);
|
||||||
} else {
|
} else {
|
||||||
|
const table = new CliTable({
|
||||||
|
head: ['Branch', ...UPDATE_COLUMNS],
|
||||||
|
wordWrap: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
table.push(
|
||||||
|
...currentPage.map(branch => {
|
||||||
|
if (branch.updates.length === 0) {
|
||||||
|
return [branch.name, 'N/A', 'N/A', 'N/A', 'N/A'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestUpdateOnBranch = branch.updates[0];
|
||||||
|
return [
|
||||||
|
branch.name,
|
||||||
|
formatUpdateMessage(latestUpdateOnBranch),
|
||||||
|
latestUpdateOnBranch.runtimeVersion,
|
||||||
|
latestUpdateOnBranch.group,
|
||||||
|
getPlatformsForGroup({
|
||||||
|
group: latestUpdateOnBranch.group,
|
||||||
|
updates: branch.updates,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Log.addNewLineIfNone();
|
Log.addNewLineIfNone();
|
||||||
Log.log(chalk.bold('Branches:'));
|
Log.log(chalk.bold('Branches:'));
|
||||||
Log.addNewLineIfNone();
|
Log.addNewLineIfNone();
|
||||||
Log.log(
|
Log.log(table.toString());
|
||||||
currentPage
|
|
||||||
.map(branch => formatBranch(getBranchDescription(branch)))
|
|
||||||
.join(`\n\n${chalk.dim('———')}\n\n`)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createUpdateBranchOnAppAsync(
|
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
|
||||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
|
||||||
const result = await withErrorHandlingAsync(
|
|
||||||
graphqlClient
|
|
||||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
|
||||||
gql`
|
|
||||||
mutation CreateUpdateBranchForApp($appId: ID!, $name: String!) {
|
|
||||||
updateBranch {
|
|
||||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
{
|
|
||||||
appId,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.toPromise()
|
|
||||||
);
|
|
||||||
const newBranch = result.updateBranch.createUpdateBranchForApp;
|
|
||||||
if (!newBranch) {
|
|
||||||
throw new Error(`Could not create branch ${name}.`);
|
|
||||||
}
|
|
||||||
return newBranch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensureBranchExistsAsync(
|
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
{
|
|
||||||
appId,
|
|
||||||
branchName,
|
|
||||||
}: {
|
|
||||||
appId: string;
|
|
||||||
branchName: string;
|
|
||||||
}
|
|
||||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
|
||||||
try {
|
|
||||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
|
||||||
appId,
|
|
||||||
name: branchName,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { id } = updateBranch;
|
|
||||||
return { branchId: id, createdBranch: false };
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof BranchNotFoundError) {
|
|
||||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
|
||||||
appId,
|
|
||||||
name: branchName,
|
|
||||||
});
|
|
||||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Client } from '../vcs/vcs';
|
import { getVcsClient } from '../vcs';
|
||||||
|
|
||||||
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
|
export async function getDefaultBranchNameAsync(): Promise<string> {
|
||||||
return (
|
return (
|
||||||
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
|
(await getVcsClient().getBranchNameAsync()) ||
|
||||||
|
`branch-${Math.random().toString(36).substring(2, 4)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { instance, mock } from 'ts-mockito';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
|
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
|
||||||
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||||
@@ -45,18 +43,14 @@ describe(selectBuildToCancelAsync.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('does not return build id when confirmation is rejected', async () => {
|
it('does not return build id when confirmation is rejected', async () => {
|
||||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
|
||||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns build id when confirmation is confirmed', async () => {
|
it('returns build id when confirmation is confirmed', async () => {
|
||||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
|
||||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
||||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
expect(selectBuildToCancelAsync(projectId, 'blah')).resolves.toEqual(selectedBuildId);
|
||||||
selectedBuildId
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,6 +79,5 @@ function createMockBuildFragment({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
status: BuildStatus.InQueue,
|
status: BuildStatus.InQueue,
|
||||||
isForIosSimulator: false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { instance, mock } from 'ts-mockito';
|
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
|
|
||||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { selectBuildToDeleteAsync } from '../../commands/build/delete';
|
|
||||||
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
|
|
||||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
|
||||||
import { confirmAsync, selectAsync } from '../../prompts';
|
|
||||||
|
|
||||||
jest.mock('../../ora', () => ({
|
|
||||||
ora: jest.fn().mockImplementation(() => ({
|
|
||||||
start: jest.fn().mockImplementation(() => ({
|
|
||||||
stop: jest.fn(),
|
|
||||||
fail: jest.fn(),
|
|
||||||
})),
|
|
||||||
})),
|
|
||||||
}));
|
|
||||||
jest.mock('../../prompts', () => {
|
|
||||||
return {
|
|
||||||
selectAsync: jest.fn(),
|
|
||||||
confirmAsync: jest.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
jest.mock('../../graphql/queries/BuildQuery', () => {
|
|
||||||
const actual = jest.requireActual('../../graphql/queries/BuildQuery');
|
|
||||||
return {
|
|
||||||
BuildQuery: {
|
|
||||||
...actual.BuildQuery,
|
|
||||||
viewBuildsOnAppAsync: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(selectBuildToDeleteAsync.name, () => {
|
|
||||||
const selectedBuildId = uuid();
|
|
||||||
const projectId = uuid();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest
|
|
||||||
.mocked(BuildQuery.viewBuildsOnAppAsync)
|
|
||||||
.mockImplementation(async () => [
|
|
||||||
createMockBuildFragment({ projectId, buildId: selectedBuildId }),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not return build id when confirmation is rejected', async () => {
|
|
||||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
|
||||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
|
||||||
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns build id when confirmation is confirmed', async () => {
|
|
||||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
|
||||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
|
||||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
|
||||||
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
|
||||||
selectedBuildId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function createMockBuildFragment({
|
|
||||||
projectId,
|
|
||||||
buildId,
|
|
||||||
}: {
|
|
||||||
projectId: string;
|
|
||||||
buildId?: string;
|
|
||||||
}): BuildFragment {
|
|
||||||
return {
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
platform: AppPlatform.Android,
|
|
||||||
id: buildId ?? uuid(),
|
|
||||||
priority: BuildPriority.Normal,
|
|
||||||
project: {
|
|
||||||
__typename: 'App',
|
|
||||||
slug: 'test-project',
|
|
||||||
id: projectId,
|
|
||||||
name: 'test-project',
|
|
||||||
ownerAccount: {
|
|
||||||
__typename: 'Account',
|
|
||||||
id: uuid(),
|
|
||||||
name: 'test-account',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
status: BuildStatus.InQueue,
|
|
||||||
isForIosSimulator: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
@@ -1,597 +0,0 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
|
||||||
import { CombinedError } from '@urql/core';
|
|
||||||
import { GraphQLError } from 'graphql/error';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
|
||||||
|
|
||||||
import { getError } from '../../__tests__/commands/utils';
|
|
||||||
import { EasCommandError } from '../../commandUtils/errors';
|
|
||||||
import Build from '../../commands/build';
|
|
||||||
import Log, { link } from '../../log';
|
|
||||||
import { handleBuildRequestError } from '../build';
|
|
||||||
import {
|
|
||||||
EasBuildDownForMaintenanceError,
|
|
||||||
EasBuildFreeTierDisabledAndroidError,
|
|
||||||
EasBuildFreeTierDisabledError,
|
|
||||||
EasBuildFreeTierDisabledIOSError,
|
|
||||||
EasBuildFreeTierIosLimitExceededError,
|
|
||||||
EasBuildFreeTierLimitExceededError,
|
|
||||||
EasBuildResourceClassNotAvailableInFreeTierError,
|
|
||||||
EasBuildTooManyPendingBuildsError,
|
|
||||||
RequestValidationError,
|
|
||||||
TurtleDeprecatedJobFormatError,
|
|
||||||
} from '../errors';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
|
|
||||||
{
|
|
||||||
"name": "GraphQLError",
|
|
||||||
"extensions": {},
|
|
||||||
"message": "Error 1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GraphQLError",
|
|
||||||
"extensions": {},
|
|
||||||
"message": "Error 2"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "GraphQLError",
|
|
||||||
"extensions": {},
|
|
||||||
"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 mockRequestId = uuidv4();
|
|
||||||
const getGraphQLError = (message: string, errorCode: string): GraphQLError => {
|
|
||||||
return new GraphQLError(message, null, null, null, null, null, {
|
|
||||||
errorCode,
|
|
||||||
requestId: mockRequestId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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('for TURTLE_DEPRECATED_JOB_FORMAT', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<TurtleDeprecatedJobFormatError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
TurtleDeprecatedJobFormatError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws correct EasBuildResourceClassNotAvailableInFreeTierError', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError(
|
|
||||||
'Error 1',
|
|
||||||
'EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER'
|
|
||||||
);
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError =
|
|
||||||
getError<EasBuildResourceClassNotAvailableInFreeTierError>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildResourceClassNotAvailableInFreeTierError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'TURTLE_DEPRECATED_JOB_FORMAT');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<TurtleDeprecatedJobFormatError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
TurtleDeprecatedJobFormatError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws correct EasBuildResourceClassNotAvailableInFreeTierError', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError(
|
|
||||||
'Error 1',
|
|
||||||
'EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER'
|
|
||||||
);
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError =
|
|
||||||
getError<EasBuildResourceClassNotAvailableInFreeTierError>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildResourceClassNotAvailableInFreeTierError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for EAS_BUILD_FREE_TIER_DISABLED', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildFreeTierDisabledError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildFreeTierDisabledError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('for EAS_BUILD_FREE_TIER_DISABLED_IOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', 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, 'Error 1');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('for EAS_BUILD_FREE_TIER_DISABLED_ANDROID', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_DISABLED_ANDROID');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierDisabledAndroidError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildFreeTierDisabledAndroidError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('for EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierLimitExceededError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildFreeTierLimitExceededError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('for EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError =
|
|
||||||
getError<EasBuildFreeTierIosLimitExceededError>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildFreeTierIosLimitExceededError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for VALIDATION_ERROR', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<RequestValidationError>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
RequestValidationError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'VALIDATION_ERROR');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<RequestValidationError>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
RequestValidationError,
|
|
||||||
'Error 1'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error is other expected graphQL-related error', () => {
|
|
||||||
describe('for EAS_BUILD_DOWN_FOR_MAINTENANCE', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildDownForMaintenanceError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildDownForMaintenanceError,
|
|
||||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_DOWN_FOR_MAINTENANCE');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildDownForMaintenanceError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildDownForMaintenanceError,
|
|
||||||
EXPECTED_EAS_BUILD_DOWN_MESSAGE
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for EAS_BUILD_TOO_MANY_PENDING_BUILDS', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildTooManyPendingBuildsError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildTooManyPendingBuildsError,
|
|
||||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_ANDROID
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws correct EasCommandError subclass', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_TOO_MANY_PENDING_BUILDS');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<EasBuildTooManyPendingBuildsError>(
|
|
||||||
() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assertReThrownError(
|
|
||||||
handleBuildRequestErrorThrownError,
|
|
||||||
EasBuildTooManyPendingBuildsError,
|
|
||||||
EXPECTED_MAX_BUILD_COUNT_MESSAGE_IOS
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error is unexpected graphQL-related error', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws base Error class with custom message', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nRequest ID: ${mockRequestId}\nError message: Error 1`;
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLErrors = [
|
|
||||||
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
|
|
||||||
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
|
|
||||||
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
|
|
||||||
];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage =
|
|
||||||
`${EXPECTED_GENERIC_MESSAGE}\n` +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 1\n' +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 2\n' +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 3';
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
describe('without request ID', () => {
|
|
||||||
it('throws base Error class with custom message without request ID line', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
|
||||||
delete graphQLError.extensions.requestId;
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nError message: Error 1`;
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws base Error class with custom message', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nRequest ID: ${mockRequestId}\nError message: Error 1`;
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLErrors = [
|
|
||||||
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
|
|
||||||
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
|
|
||||||
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
|
|
||||||
];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage =
|
|
||||||
`${EXPECTED_GENERIC_MESSAGE}\n` +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 1\n' +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 2\n' +
|
|
||||||
`Request ID: ${mockRequestId}\n` +
|
|
||||||
'Error message: Error 3';
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
describe('without request ID', () => {
|
|
||||||
it('throws base Error class with custom message without request ID line', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const graphQLError = getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR');
|
|
||||||
delete graphQLError.extensions.requestId;
|
|
||||||
const graphQLErrors = [graphQLError];
|
|
||||||
const error = new CombinedError({ graphQLErrors });
|
|
||||||
const expectedMessage = `${EXPECTED_GENERIC_MESSAGE}\nError message: Error 1`;
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('error is unexpected non-graphQL-related error', () => {
|
|
||||||
describe('with Android', () => {
|
|
||||||
it('throws base Error class with propagated message', async () => {
|
|
||||||
const platform = Platform.ANDROID;
|
|
||||||
const error = new Error('Non-graphQL-related error');
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleBuildRequestErrorThrownError).toStrictEqual(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with iOS', () => {
|
|
||||||
it('throws base Error class with propagated message', async () => {
|
|
||||||
const platform = Platform.IOS;
|
|
||||||
const error = new Error('Non-graphQL-related error');
|
|
||||||
|
|
||||||
const handleBuildRequestErrorThrownError = getError<Error>(() => {
|
|
||||||
handleBuildRequestError(error, platform);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(handleBuildRequestErrorThrownError).toStrictEqual(error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
|
||||||
import { ExitError } from '@oclif/core/lib/errors';
|
|
||||||
import path from 'path';
|
|
||||||
import { instance, mock, when } from 'ts-mockito';
|
|
||||||
|
|
||||||
import { CommonContext } from '../context';
|
|
||||||
import { validatePNGsForManagedProjectAsync } from '../validate';
|
|
||||||
|
|
||||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
|
||||||
|
|
||||||
describe(validatePNGsForManagedProjectAsync, () => {
|
|
||||||
it('does not validate PNGs for generic projects', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not validate PNGs for projects with SDK >= 47', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.GENERIC);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
|
||||||
sdkVersion: '47.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(Platform.ANDROID, () => {
|
|
||||||
it('exits if foregroundImage is not a file with .png extension', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
android: { adaptiveIcon: { foregroundImage: 'assets/icon.jpg' } },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits if foregroundImage is not a png file', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-jpg.png') } },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not throw if foregroundImage is a png file', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.ANDROID>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.ANDROID);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
android: { adaptiveIcon: { foregroundImage: path.join(fixturesPath, 'icon-alpha.png') } },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validating iOS PNGs is currently disabled
|
|
||||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
|
||||||
xdescribe(Platform.IOS, () => {
|
|
||||||
it('exits if icon is not a file with .png extension', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
ios: { icon: path.join(fixturesPath, 'assets/icon.jpg') },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits if icon is not a png file', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
ios: { icon: path.join(fixturesPath, 'icon-jpg.png') },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('exits if icon has alpha channel (transparency)', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
ios: { icon: path.join(fixturesPath, 'icon-alpha.png') },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).rejects.toThrow(ExitError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not throw if icon is a png file and does not have alpha channel', async () => {
|
|
||||||
const ctxMock = mock<CommonContext<Platform.IOS>>();
|
|
||||||
when(ctxMock.workflow).thenReturn(Workflow.MANAGED);
|
|
||||||
when(ctxMock.platform).thenReturn(Platform.IOS);
|
|
||||||
when(ctxMock.exp).thenReturn({
|
|
||||||
name: 'blah',
|
|
||||||
slug: 'blah',
|
|
||||||
ios: { icon: path.join(fixturesPath, 'icon-no-alpha.png') },
|
|
||||||
sdkVersion: '46.0.0',
|
|
||||||
});
|
|
||||||
const ctx = instance(ctxMock);
|
|
||||||
|
|
||||||
await expect(validatePNGsForManagedProjectAsync(ctx)).resolves.not.toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,6 @@ import os from 'os';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||||
import { resolveVcsClient } from '../../../vcs';
|
|
||||||
import {
|
import {
|
||||||
BumpStrategy,
|
BumpStrategy,
|
||||||
bumpVersionAsync,
|
bumpVersionAsync,
|
||||||
@@ -19,8 +18,6 @@ import {
|
|||||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||||
jest.mock('fs');
|
jest.mock('fs');
|
||||||
|
|
||||||
const vcsClient = resolveVcsClient();
|
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
// do not remove the following line
|
// do not remove the following line
|
||||||
// this fixes a weird error with tempy in @expo/image-utils
|
// this fixes a weird error with tempy in @expo/image-utils
|
||||||
@@ -194,8 +191,7 @@ describe(maybeResolveVersionsAsync, () => {
|
|||||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||||
'/app',
|
'/app',
|
||||||
exp,
|
exp,
|
||||||
{} as BuildProfile<Platform.ANDROID>,
|
{} as BuildProfile<Platform.ANDROID>
|
||||||
vcsClient
|
|
||||||
);
|
);
|
||||||
expect(appVersion).toBe('3.0.0');
|
expect(appVersion).toBe('3.0.0');
|
||||||
expect(appBuildVersion).toBe('123');
|
expect(appBuildVersion).toBe('123');
|
||||||
@@ -207,8 +203,7 @@ describe(maybeResolveVersionsAsync, () => {
|
|||||||
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
|
||||||
'/app',
|
'/app',
|
||||||
exp,
|
exp,
|
||||||
{} as BuildProfile<Platform.ANDROID>,
|
{} as BuildProfile<Platform.ANDROID>
|
||||||
vcsClient
|
|
||||||
);
|
);
|
||||||
expect(appVersion).toBe('5.0.0');
|
expect(appVersion).toBe('5.0.0');
|
||||||
expect(appBuildVersion).toBe('126');
|
expect(appBuildVersion).toBe('126');
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { AppVersionSource } from '@expo/eas-json';
|
import { AppVersionSource } from '@expo/eas-json';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
|
|
||||||
import { transformJob } from './graphql';
|
|
||||||
import { prepareJobAsync } from './prepareJob';
|
|
||||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
|
||||||
import { resolveRemoteVersionCodeAsync } from './version';
|
|
||||||
import AndroidCredentialsProvider, {
|
import AndroidCredentialsProvider, {
|
||||||
AndroidCredentials,
|
AndroidCredentials,
|
||||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||||
@@ -19,6 +15,7 @@ import {
|
|||||||
} from '../../project/android/applicationId';
|
} from '../../project/android/applicationId';
|
||||||
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
import { resolveGradleBuildContextAsync } from '../../project/android/gradle';
|
||||||
import { toggleConfirmAsync } from '../../prompts';
|
import { toggleConfirmAsync } from '../../prompts';
|
||||||
|
import { findAccountByName } from '../../user/Account';
|
||||||
import {
|
import {
|
||||||
BuildRequestSender,
|
BuildRequestSender,
|
||||||
CredentialsResult,
|
CredentialsResult,
|
||||||
@@ -28,11 +25,11 @@ import {
|
|||||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||||
import { transformMetadata } from '../graphql';
|
import { transformMetadata } from '../graphql';
|
||||||
import { logCredentialsSource } from '../utils/credentials';
|
import { logCredentialsSource } from '../utils/credentials';
|
||||||
import {
|
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||||
checkGoogleServicesFileAsync,
|
import { transformJob } from './graphql';
|
||||||
checkNodeEnvVariable,
|
import { prepareJobAsync } from './prepareJob';
|
||||||
validatePNGsForManagedProjectAsync,
|
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||||
} from '../validate';
|
import { resolveRemoteVersionCodeAsync } from './version';
|
||||||
|
|
||||||
export async function createAndroidContextAsync(
|
export async function createAndroidContextAsync(
|
||||||
ctx: CommonContext<Platform.ANDROID>
|
ctx: CommonContext<Platform.ANDROID>
|
||||||
@@ -57,33 +54,22 @@ This means that it will most likely produce an AAB and you will not be able to i
|
|||||||
|
|
||||||
checkNodeEnvVariable(ctx);
|
checkNodeEnvVariable(ctx);
|
||||||
await checkGoogleServicesFileAsync(ctx);
|
await checkGoogleServicesFileAsync(ctx);
|
||||||
await validatePNGsForManagedProjectAsync(ctx);
|
|
||||||
|
|
||||||
const gradleContext = await resolveGradleBuildContextAsync(
|
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
|
||||||
ctx.projectDir,
|
|
||||||
buildProfile,
|
|
||||||
ctx.vcsClient
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ctx.workflow === Workflow.MANAGED) {
|
if (ctx.workflow === Workflow.MANAGED) {
|
||||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
|
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
const applicationId = await getApplicationIdAsync(
|
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
|
||||||
ctx.projectDir,
|
|
||||||
ctx.exp,
|
|
||||||
ctx.vcsClient,
|
|
||||||
gradleContext
|
|
||||||
);
|
|
||||||
const versionCodeOverride =
|
const versionCodeOverride =
|
||||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||||
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
|
? await resolveRemoteVersionCodeAsync({
|
||||||
projectDir: ctx.projectDir,
|
projectDir: ctx.projectDir,
|
||||||
projectId: ctx.projectId,
|
projectId: ctx.projectId,
|
||||||
exp: ctx.exp,
|
exp: ctx.exp,
|
||||||
applicationId,
|
applicationId,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
vcsClient: ctx.vcsClient,
|
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@@ -106,14 +92,13 @@ export async function prepareAndroidBuildAsync(
|
|||||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||||
? false
|
? false
|
||||||
: ctx.buildProfile.autoIncrement,
|
: ctx.buildProfile.autoIncrement,
|
||||||
vcsClient: ctx.vcsClient,
|
nonInteractive: ctx.nonInteractive,
|
||||||
env: ctx.buildProfile.env,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
prepareJobAsync: async (
|
prepareJobAsync: async (
|
||||||
ctx: BuildContext<Platform.ANDROID>,
|
ctx: BuildContext<Platform.ANDROID>,
|
||||||
jobData: JobData<AndroidCredentials>
|
jobData: JobData<AndroidCredentials>
|
||||||
): Promise<Android.Job> => {
|
): Promise<Job> => {
|
||||||
return await prepareJobAsync(ctx, jobData);
|
return await prepareJobAsync(ctx, jobData);
|
||||||
},
|
},
|
||||||
sendBuildRequestAsync: async (
|
sendBuildRequestAsync: async (
|
||||||
@@ -124,7 +109,7 @@ export async function prepareAndroidBuildAsync(
|
|||||||
): Promise<BuildResult> => {
|
): Promise<BuildResult> => {
|
||||||
const graphqlMetadata = transformMetadata(metadata);
|
const graphqlMetadata = transformMetadata(metadata);
|
||||||
const graphqlJob = transformJob(job);
|
const graphqlJob = transformJob(job);
|
||||||
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
|
return await BuildMutation.createAndroidBuildAsync({
|
||||||
appId,
|
appId,
|
||||||
job: graphqlJob,
|
job: graphqlJob,
|
||||||
metadata: graphqlMetadata,
|
metadata: graphqlMetadata,
|
||||||
@@ -147,13 +132,12 @@ async function ensureAndroidCredentialsAsync(
|
|||||||
const androidApplicationIdentifier = await getApplicationIdAsync(
|
const androidApplicationIdentifier = await getApplicationIdAsync(
|
||||||
ctx.projectDir,
|
ctx.projectDir,
|
||||||
ctx.exp,
|
ctx.exp,
|
||||||
ctx.vcsClient,
|
|
||||||
ctx.android.gradleContext
|
ctx.android.gradleContext
|
||||||
);
|
);
|
||||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||||
app: {
|
app: {
|
||||||
account: nullthrows(
|
account: nullthrows(
|
||||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
findAccountByName(ctx.user.accounts, ctx.accountName),
|
||||||
`You do not have access to account: ${ctx.accountName}`
|
`You do not have access to account: ${ctx.accountName}`
|
||||||
),
|
),
|
||||||
projectName: ctx.projectName,
|
projectName: ctx.projectName,
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
import { Android } from '@expo/eas-build-job';
|
import { Android } from '@expo/eas-build-job';
|
||||||
|
|
||||||
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
|
||||||
import {
|
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||||
loggerLevelToGraphQLWorkerLoggerLevel,
|
|
||||||
transformBuildMode,
|
|
||||||
transformBuildTrigger,
|
|
||||||
transformProjectArchive,
|
|
||||||
transformWorkflow,
|
|
||||||
} from '../graphql';
|
|
||||||
|
|
||||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||||
return {
|
return {
|
||||||
type: transformWorkflow(job.type),
|
type: transformWorkflow(job.type),
|
||||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
|
||||||
projectArchive: transformProjectArchive(job.projectArchive),
|
projectArchive: transformProjectArchive(job.projectArchive),
|
||||||
projectRootDirectory: job.projectRootDirectory,
|
projectRootDirectory: job.projectRootDirectory,
|
||||||
|
releaseChannel: job.releaseChannel,
|
||||||
updates: job.updates,
|
updates: job.updates,
|
||||||
secrets: job.secrets,
|
secrets: job.secrets,
|
||||||
builderEnvironment: job.builderEnvironment,
|
builderEnvironment: job.builderEnvironment,
|
||||||
@@ -27,11 +21,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
|||||||
buildType: job.buildType && transformBuildType(job.buildType),
|
buildType: job.buildType && transformBuildType(job.buildType),
|
||||||
developmentClient: job.developmentClient,
|
developmentClient: job.developmentClient,
|
||||||
experimental: job.experimental,
|
experimental: job.experimental,
|
||||||
mode: transformBuildMode(job.mode),
|
|
||||||
customBuildConfig: job.customBuildConfig,
|
|
||||||
loggerLevel: job.loggerLevel
|
|
||||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
import {
|
import { Android, ArchiveSource, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||||
Android,
|
|
||||||
ArchiveSource,
|
|
||||||
BuildMode,
|
|
||||||
BuildTrigger,
|
|
||||||
Platform,
|
|
||||||
sanitizeBuildJob,
|
|
||||||
} from '@expo/eas-build-job';
|
|
||||||
import { BuildProfile } from '@expo/eas-json';
|
import { BuildProfile } from '@expo/eas-json';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
|
|
||||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||||
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
|
|
||||||
import { getUsername } from '../../project/projectUtils';
|
import { getUsername } from '../../project/projectUtils';
|
||||||
|
import { ensureLoggedInAsync } from '../../user/actions';
|
||||||
|
import { getVcsClient } from '../../vcs';
|
||||||
import { BuildContext } from '../context';
|
import { BuildContext } from '../context';
|
||||||
|
|
||||||
interface JobData {
|
interface JobData {
|
||||||
@@ -22,17 +16,21 @@ interface JobData {
|
|||||||
|
|
||||||
const cacheDefaults = {
|
const cacheDefaults = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
paths: [],
|
customPaths: [],
|
||||||
|
cacheDefaultPaths: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function prepareJobAsync(
|
export async function prepareJobAsync(
|
||||||
ctx: BuildContext<Platform.ANDROID>,
|
ctx: BuildContext<Platform.ANDROID>,
|
||||||
jobData: JobData
|
jobData: JobData
|
||||||
): Promise<Android.Job> {
|
): Promise<Job> {
|
||||||
const username = getUsername(ctx.exp, ctx.user);
|
const username = getUsername(
|
||||||
|
ctx.exp,
|
||||||
|
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
|
||||||
|
);
|
||||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||||
const projectRootDirectory =
|
const projectRootDirectory =
|
||||||
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
|
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||||
const { credentials } = jobData;
|
const { credentials } = jobData;
|
||||||
const buildCredentials = credentials
|
const buildCredentials = credentials
|
||||||
? {
|
? {
|
||||||
@@ -47,24 +45,11 @@ export async function prepareJobAsync(
|
|||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
let buildType = buildProfile.buildType;
|
let buildType = ctx.buildProfile.buildType;
|
||||||
if (!buildType && !buildProfile.gradleCommand && buildProfile.distribution === 'internal') {
|
if (!buildType && !buildProfile.gradleCommand && ctx.buildProfile.distribution === 'internal') {
|
||||||
buildType = Android.BuildType.APK;
|
buildType = Android.BuildType.APK;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeCustomBuildConfigPath = buildProfile.config
|
|
||||||
? getCustomBuildConfigPathForJob(buildProfile.config)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let buildMode;
|
|
||||||
if (ctx.repack) {
|
|
||||||
buildMode = BuildMode.REPACK;
|
|
||||||
} else if (buildProfile.config) {
|
|
||||||
buildMode = BuildMode.CUSTOM;
|
|
||||||
} else {
|
|
||||||
buildMode = BuildMode.BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
const job: Android.Job = {
|
const job: Android.Job = {
|
||||||
type: ctx.workflow,
|
type: ctx.workflow,
|
||||||
platform: Platform.ANDROID,
|
platform: Platform.ANDROID,
|
||||||
@@ -73,10 +58,9 @@ export async function prepareJobAsync(
|
|||||||
builderEnvironment: {
|
builderEnvironment: {
|
||||||
image: buildProfile.image,
|
image: buildProfile.image,
|
||||||
node: buildProfile.node,
|
node: buildProfile.node,
|
||||||
pnpm: buildProfile.pnpm,
|
|
||||||
bun: buildProfile.bun,
|
|
||||||
yarn: buildProfile.yarn,
|
yarn: buildProfile.yarn,
|
||||||
ndk: buildProfile.ndk,
|
ndk: buildProfile.ndk,
|
||||||
|
expoCli: buildProfile.expoCli,
|
||||||
env: buildProfile.env,
|
env: buildProfile.env,
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
@@ -87,7 +71,8 @@ export async function prepareJobAsync(
|
|||||||
secrets: {
|
secrets: {
|
||||||
...buildCredentials,
|
...buildCredentials,
|
||||||
},
|
},
|
||||||
updates: { channel: buildProfile.channel },
|
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||||
|
updates: { channel: ctx.buildProfile.channel },
|
||||||
developmentClient: buildProfile.developmentClient,
|
developmentClient: buildProfile.developmentClient,
|
||||||
gradleCommand: buildProfile.gradleCommand,
|
gradleCommand: buildProfile.gradleCommand,
|
||||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||||
@@ -100,22 +85,9 @@ export async function prepareJobAsync(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
experimental: {
|
experimental: {
|
||||||
prebuildCommand: buildProfile.prebuildCommand,
|
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||||
},
|
},
|
||||||
mode: buildMode,
|
|
||||||
triggeredBy: BuildTrigger.EAS_CLI,
|
|
||||||
...(maybeCustomBuildConfigPath && {
|
|
||||||
customBuildConfig: {
|
|
||||||
path: maybeCustomBuildConfigPath,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(ctx.repack && {
|
|
||||||
customBuildConfig: {
|
|
||||||
path: '__eas/repack.yml',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
loggerLevel: ctx.loggerLevel,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return sanitizeBuildJob(job) as Android.Job;
|
return sanitizeJob(job);
|
||||||
}
|
}
|
||||||
@@ -1,38 +1,35 @@
|
|||||||
import { ExpoConfig } from '@expo/config';
|
import { ExpoConfig } from '@expo/config';
|
||||||
import { AndroidConfig } from '@expo/config-plugins';
|
import { AndroidConfig } from '@expo/config-plugins';
|
||||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
|
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
|
||||||
import Log from '../../log';
|
import Log from '../../log';
|
||||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||||
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
|
||||||
import { Client } from '../../vcs/vcs';
|
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||||
|
|
||||||
export async function syncProjectConfigurationAsync({
|
export async function syncProjectConfigurationAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
localAutoIncrement,
|
localAutoIncrement,
|
||||||
vcsClient,
|
nonInteractive,
|
||||||
env,
|
|
||||||
}: {
|
}: {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
exp: ExpoConfig;
|
exp: ExpoConfig;
|
||||||
localAutoIncrement?: AndroidVersionAutoIncrement;
|
localAutoIncrement?: AndroidVersionAutoIncrement;
|
||||||
vcsClient: Client;
|
nonInteractive: boolean;
|
||||||
env: Env | undefined;
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
|
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||||
|
|
||||||
if (workflow === Workflow.GENERIC) {
|
if (workflow === Workflow.GENERIC) {
|
||||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||||
if (isExpoUpdatesInstalled(projectDir)) {
|
if (isExpoUpdatesInstalled(projectDir)) {
|
||||||
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
|
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive });
|
||||||
}
|
}
|
||||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { BuildProfile } from '@expo/eas-json';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { AppPlatform } from '../../graphql/generated';
|
import { AppPlatform } from '../../graphql/generated';
|
||||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||||
@@ -18,7 +17,6 @@ import {
|
|||||||
} from '../../project/android/gradleUtils';
|
} from '../../project/android/gradleUtils';
|
||||||
import { getNextVersionCode } from '../../project/android/versions';
|
import { getNextVersionCode } from '../../project/android/versions';
|
||||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||||
import { Client } from '../../vcs/vcs';
|
|
||||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||||
|
|
||||||
@@ -97,10 +95,9 @@ export async function bumpVersionInAppJsonAsync({
|
|||||||
export async function maybeResolveVersionsAsync(
|
export async function maybeResolveVersionsAsync(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
exp: ExpoConfig,
|
exp: ExpoConfig,
|
||||||
buildProfile: BuildProfile<Platform.ANDROID>,
|
buildProfile: BuildProfile<Platform.ANDROID>
|
||||||
vcsClient: Client
|
|
||||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
|
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
|
||||||
if (workflow === Workflow.GENERIC) {
|
if (workflow === Workflow.GENERIC) {
|
||||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||||
try {
|
try {
|
||||||
@@ -177,32 +174,26 @@ async function writeBuildGradleAsync({
|
|||||||
* Returns buildNumber that will be used for the next build. If current build profile
|
* Returns buildNumber that will be used for the next build. If current build profile
|
||||||
* has an 'autoIncrement' option set, it increments the version on server.
|
* has an 'autoIncrement' option set, it increments the version on server.
|
||||||
*/
|
*/
|
||||||
export async function resolveRemoteVersionCodeAsync(
|
export async function resolveRemoteVersionCodeAsync({
|
||||||
graphqlClient: ExpoGraphqlClient,
|
projectDir,
|
||||||
{
|
projectId,
|
||||||
projectDir,
|
exp,
|
||||||
projectId,
|
applicationId,
|
||||||
exp,
|
buildProfile,
|
||||||
applicationId,
|
}: {
|
||||||
buildProfile,
|
projectDir: string;
|
||||||
vcsClient,
|
projectId: string;
|
||||||
}: {
|
exp: ExpoConfig;
|
||||||
projectDir: string;
|
applicationId: string;
|
||||||
projectId: string;
|
buildProfile: BuildProfile<Platform.ANDROID>;
|
||||||
exp: ExpoConfig;
|
}): Promise<string> {
|
||||||
applicationId: string;
|
|
||||||
buildProfile: BuildProfile<Platform.ANDROID>;
|
|
||||||
vcsClient: Client;
|
|
||||||
}
|
|
||||||
): Promise<string> {
|
|
||||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||||
graphqlClient,
|
|
||||||
projectId,
|
projectId,
|
||||||
AppPlatform.Android,
|
AppPlatform.Android,
|
||||||
applicationId
|
applicationId
|
||||||
);
|
);
|
||||||
|
|
||||||
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
|
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
|
||||||
let currentBuildVersion: string;
|
let currentBuildVersion: string;
|
||||||
if (remoteVersions?.buildVersion) {
|
if (remoteVersions?.buildVersion) {
|
||||||
currentBuildVersion = remoteVersions.buildVersion;
|
currentBuildVersion = remoteVersions.buildVersion;
|
||||||
@@ -226,15 +217,13 @@ export async function resolveRemoteVersionCodeAsync(
|
|||||||
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
|
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
|
||||||
).start();
|
).start();
|
||||||
try {
|
try {
|
||||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
await AppVersionMutation.createAppVersionAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
platform: AppPlatform.Android,
|
platform: AppPlatform.Android,
|
||||||
applicationIdentifier: applicationId,
|
applicationIdentifier: applicationId,
|
||||||
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
||||||
buildVersion: currentBuildVersion,
|
buildVersion: currentBuildVersion,
|
||||||
runtimeVersion:
|
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
|
||||||
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
|
|
||||||
undefined,
|
|
||||||
});
|
});
|
||||||
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
|
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -250,15 +239,13 @@ export async function resolveRemoteVersionCodeAsync(
|
|||||||
)}.`
|
)}.`
|
||||||
).start();
|
).start();
|
||||||
try {
|
try {
|
||||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
await AppVersionMutation.createAppVersionAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
platform: AppPlatform.Android,
|
platform: AppPlatform.Android,
|
||||||
applicationIdentifier: applicationId,
|
applicationIdentifier: applicationId,
|
||||||
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
|
||||||
buildVersion: String(nextBuildVersion),
|
buildVersion: String(nextBuildVersion),
|
||||||
runtimeVersion:
|
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
|
||||||
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
|
|
||||||
undefined,
|
|
||||||
});
|
});
|
||||||
spinner.succeed(
|
spinner.succeed(
|
||||||
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||||
|
|||||||
@@ -1,48 +1,13 @@
|
|||||||
import {
|
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
|
||||||
ArchiveSource,
|
|
||||||
ArchiveSourceType,
|
|
||||||
BuildJob,
|
|
||||||
Metadata,
|
|
||||||
Platform,
|
|
||||||
} from '@expo/eas-build-job';
|
|
||||||
import { CredentialsSource } from '@expo/eas-json';
|
import { CredentialsSource } from '@expo/eas-json';
|
||||||
import assert from 'assert';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import cliProgress from 'cli-progress';
|
import cliProgress from 'cli-progress';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import { GraphQLError } from 'graphql/error';
|
|
||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
|
|
||||||
import { BuildContext } from './context';
|
|
||||||
import {
|
|
||||||
EasBuildDownForMaintenanceError,
|
|
||||||
EasBuildFreeTierDisabledAndroidError,
|
|
||||||
EasBuildFreeTierDisabledError,
|
|
||||||
EasBuildFreeTierDisabledIOSError,
|
|
||||||
EasBuildFreeTierIosLimitExceededError,
|
|
||||||
EasBuildFreeTierLimitExceededError,
|
|
||||||
EasBuildLegacyResourceClassNotAvailableError,
|
|
||||||
EasBuildProjectArchiveUploadError,
|
|
||||||
EasBuildResourceClassNotAvailableInFreeTierError,
|
|
||||||
EasBuildTooManyPendingBuildsError,
|
|
||||||
RequestValidationError,
|
|
||||||
TurtleDeprecatedJobFormatError,
|
|
||||||
} from './errors';
|
|
||||||
import { transformMetadata } from './graphql';
|
|
||||||
import { LocalBuildMode, runLocalBuildAsync } from './local';
|
|
||||||
import { collectMetadataAsync } from './metadata';
|
|
||||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
|
||||||
import {
|
|
||||||
LocalFile,
|
|
||||||
makeProjectMetadataFileAsync,
|
|
||||||
makeProjectTarballAsync,
|
|
||||||
reviewAndCommitChangesAsync,
|
|
||||||
} from './utils/repository';
|
|
||||||
import { BuildEvent } from '../analytics/AnalyticsManager';
|
|
||||||
import { withAnalyticsAsync } from '../analytics/common';
|
import { withAnalyticsAsync } from '../analytics/common';
|
||||||
|
import { BuildEvent } from '../analytics/events';
|
||||||
import { getExpoWebsiteBaseUrl } from '../api';
|
import { getExpoWebsiteBaseUrl } from '../api';
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { EasCommandError } from '../commandUtils/errors';
|
|
||||||
import {
|
import {
|
||||||
AppPlatform,
|
AppPlatform,
|
||||||
BuildFragment,
|
BuildFragment,
|
||||||
@@ -51,20 +16,25 @@ import {
|
|||||||
BuildStatus,
|
BuildStatus,
|
||||||
UploadSessionType,
|
UploadSessionType,
|
||||||
} from '../graphql/generated';
|
} from '../graphql/generated';
|
||||||
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
|
import { BuildResult } from '../graphql/mutations/BuildMutation';
|
||||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||||
import Log, { learnMore, link } from '../log';
|
import Log, { learnMore } from '../log';
|
||||||
import { Ora, ora } from '../ora';
|
import { Ora, ora } from '../ora';
|
||||||
import {
|
import {
|
||||||
appPlatformDisplayNames,
|
appPlatformDisplayNames,
|
||||||
appPlatformEmojis,
|
appPlatformEmojis,
|
||||||
requestedPlatformDisplayNames,
|
requestedPlatformDisplayNames,
|
||||||
} from '../platform';
|
} from '../platform';
|
||||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
import { uploadFileAtPathToS3Async } from '../uploads';
|
||||||
import { formatBytes } from '../utils/files';
|
import { formatBytes } from '../utils/files';
|
||||||
import { printJsonOnlyOutput } from '../utils/json';
|
|
||||||
import { createProgressTracker } from '../utils/progress';
|
import { createProgressTracker } from '../utils/progress';
|
||||||
import { sleepAsync } from '../utils/promise';
|
import { sleepAsync } from '../utils/promise';
|
||||||
|
import { getVcsClient } from '../vcs';
|
||||||
|
import { BuildContext } from './context';
|
||||||
|
import { runLocalBuildAsync } from './local';
|
||||||
|
import { collectMetadataAsync } from './metadata';
|
||||||
|
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||||
|
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||||
|
|
||||||
export interface CredentialsResult<Credentials> {
|
export interface CredentialsResult<Credentials> {
|
||||||
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
|
||||||
@@ -76,14 +46,14 @@ export interface JobData<Credentials> {
|
|||||||
projectArchive: ArchiveSource;
|
projectArchive: ArchiveSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob> {
|
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
|
||||||
ctx: BuildContext<TPlatform>;
|
ctx: BuildContext<TPlatform>;
|
||||||
|
|
||||||
ensureCredentialsAsync(
|
ensureCredentialsAsync(
|
||||||
ctx: BuildContext<TPlatform>
|
ctx: BuildContext<TPlatform>
|
||||||
): Promise<CredentialsResult<Credentials> | undefined>;
|
): Promise<CredentialsResult<Credentials> | undefined>;
|
||||||
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
|
||||||
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<BuildJob>;
|
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
|
||||||
sendBuildRequestAsync(
|
sendBuildRequestAsync(
|
||||||
appId: string,
|
appId: string,
|
||||||
job: TJob,
|
job: TJob,
|
||||||
@@ -94,148 +64,100 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob
|
|||||||
|
|
||||||
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
export type BuildRequestSender = () => Promise<BuildFragment | undefined>;
|
||||||
|
|
||||||
function resolveBuildParamsInput<T extends Platform>(
|
function resolveBuildParamsInput<T extends Platform>(ctx: BuildContext<T>): BuildParamsInput {
|
||||||
ctx: BuildContext<T>,
|
|
||||||
metadata: Metadata
|
|
||||||
): BuildParamsInput {
|
|
||||||
return {
|
return {
|
||||||
resourceClass: ctx.resourceClass,
|
resourceClass: ctx.resourceClass,
|
||||||
sdkVersion: metadata.sdkVersion,
|
|
||||||
reactNativeVersion: metadata.reactNativeVersion,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareBuildRequestForPlatformAsync<
|
export async function prepareBuildRequestForPlatformAsync<
|
||||||
TPlatform extends Platform,
|
TPlatform extends Platform,
|
||||||
Credentials,
|
Credentials,
|
||||||
TJob extends BuildJob,
|
TJob extends Job
|
||||||
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
|
||||||
const { ctx } = builder;
|
const { ctx } = builder;
|
||||||
const credentialsResult = await withAnalyticsAsync(
|
const credentialsResult = await withAnalyticsAsync(
|
||||||
ctx.analytics,
|
|
||||||
async () => await builder.ensureCredentialsAsync(ctx),
|
async () => await builder.ensureCredentialsAsync(ctx),
|
||||||
{
|
{
|
||||||
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
attemptEvent: BuildEvent.GATHER_CREDENTIALS_ATTEMPT,
|
||||||
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
successEvent: BuildEvent.GATHER_CREDENTIALS_SUCCESS,
|
||||||
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
failureEvent: BuildEvent.GATHER_CREDENTIALS_FAIL,
|
||||||
properties: ctx.analyticsEventProperties,
|
trackingCtx: ctx.trackingCtx,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
await withAnalyticsAsync(
|
await withAnalyticsAsync(async () => await builder.syncProjectConfigurationAsync(ctx), {
|
||||||
ctx.analytics,
|
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||||
async () => await builder.syncProjectConfigurationAsync(ctx),
|
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||||
{
|
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
||||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
trackingCtx: ctx.trackingCtx,
|
||||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
});
|
||||||
failureEvent: BuildEvent.CONFIGURE_PROJECT_FAIL,
|
|
||||||
properties: ctx.analyticsEventProperties,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (await ctx.vcsClient.isCommitRequiredAsync()) {
|
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||||
Log.addNewLineIfNone();
|
Log.addNewLineIfNone();
|
||||||
await reviewAndCommitChangesAsync(
|
await reviewAndCommitChangesAsync(
|
||||||
ctx.vcsClient,
|
|
||||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||||
{ nonInteractive: ctx.nonInteractive }
|
{ nonInteractive: ctx.nonInteractive }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let projectArchive: ArchiveSource | undefined;
|
const projectArchive = ctx.localBuildOptions.enable
|
||||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
? ({
|
||||||
const projectPath = (await makeProjectTarballAsync(ctx.vcsClient)).path;
|
type: ArchiveSourceType.PATH,
|
||||||
projectArchive = {
|
path: (await makeProjectTarballAsync()).path,
|
||||||
type: ArchiveSourceType.PATH,
|
} as const)
|
||||||
path: projectPath,
|
: ({
|
||||||
};
|
type: ArchiveSourceType.S3,
|
||||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
bucketKey: await uploadProjectAsync(ctx),
|
||||||
projectArchive = {
|
} as const);
|
||||||
type: ArchiveSourceType.PATH,
|
|
||||||
path: process.cwd(),
|
|
||||||
};
|
|
||||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
|
||||||
projectArchive = {
|
|
||||||
type: ArchiveSourceType.GCS,
|
|
||||||
...(await uploadProjectAsync(ctx)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
assert(projectArchive);
|
|
||||||
|
|
||||||
const metadata = await collectMetadataAsync(ctx);
|
const metadata = await collectMetadataAsync(ctx);
|
||||||
const buildParams = resolveBuildParamsInput(ctx, metadata);
|
const buildParams = resolveBuildParamsInput(ctx);
|
||||||
const job = await builder.prepareJobAsync(ctx, {
|
const job = await builder.prepareJobAsync(ctx, {
|
||||||
projectArchive,
|
projectArchive,
|
||||||
credentials: credentialsResult?.credentials,
|
credentials: credentialsResult?.credentials,
|
||||||
});
|
});
|
||||||
|
|
||||||
return async () => {
|
return async () => {
|
||||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
if (ctx.localBuildOptions.enable) {
|
||||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||||
return undefined;
|
return undefined;
|
||||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
} else {
|
||||||
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 {
|
try {
|
||||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
handleBuildRequestError(error, job.platform);
|
handleBuildRequestError(error, job.platform);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error('Unknown localBuildMode.');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
|
function handleBuildRequestError(error: any, platform: Platform): never {
|
||||||
TURTLE_DEPRECATED_JOB_FORMAT: TurtleDeprecatedJobFormatError,
|
if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'TURTLE_DEPRECATED_JOB_FORMAT') {
|
||||||
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
|
Log.error('EAS Build API has changed. Upgrade to the latest eas-cli version.');
|
||||||
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
|
throw new Error('Build request failed.');
|
||||||
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
|
} else if (
|
||||||
EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED: EasBuildFreeTierLimitExceededError,
|
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_DOWN_FOR_MAINTENANCE'
|
||||||
EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED: EasBuildFreeTierIosLimitExceededError,
|
) {
|
||||||
EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER:
|
Log.error(
|
||||||
EasBuildResourceClassNotAvailableInFreeTierError,
|
'EAS Build is down for maintenance. Try again later. Check https://status.expo.dev/ for updates.'
|
||||||
EAS_BUILD_LEGACY_RESOURCE_CLASS_NOT_AVAILABLE: EasBuildLegacyResourceClassNotAvailableError,
|
|
||||||
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.`
|
|
||||||
);
|
);
|
||||||
} else if (graphQLErrorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS') {
|
throw new Error('Build request failed.');
|
||||||
throw new EasBuildTooManyPendingBuildsError(
|
} else if (error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_FREE_TIER_DISABLED') {
|
||||||
|
Log.error(
|
||||||
|
'EAS Build free tier is temporarily disabled. Try again later. Check https://status.expo.dev/ for updates.'
|
||||||
|
);
|
||||||
|
throw new Error('Build request failed.');
|
||||||
|
} else if (
|
||||||
|
error?.graphQLErrors?.[0]?.extensions?.errorCode === 'EAS_BUILD_TOO_MANY_PENDING_BUILDS'
|
||||||
|
) {
|
||||||
|
Log.error(
|
||||||
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
|
`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) {
|
} else if (error?.graphQLErrors) {
|
||||||
const errorMessage = error.graphQLErrors
|
Log.error(
|
||||||
.map((graphQLError: GraphQLError) => {
|
'Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.'
|
||||||
const requestIdLine = graphQLError?.extensions?.requestId
|
|
||||||
? `\nRequest ID: ${graphQLError.extensions.requestId}`
|
|
||||||
: '';
|
|
||||||
const errorMessageLine = graphQLError?.message
|
|
||||||
? `\nError message: ${graphQLError.message}`
|
|
||||||
: '';
|
|
||||||
return `${requestIdLine}${errorMessageLine}`;
|
|
||||||
})
|
|
||||||
.join('');
|
|
||||||
throw new Error(
|
|
||||||
`Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.${errorMessage}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -243,14 +165,10 @@ export function handleBuildRequestError(error: any, platform: Platform): never {
|
|||||||
|
|
||||||
async function uploadProjectAsync<TPlatform extends Platform>(
|
async function uploadProjectAsync<TPlatform extends Platform>(
|
||||||
ctx: BuildContext<TPlatform>
|
ctx: BuildContext<TPlatform>
|
||||||
): Promise<{
|
): Promise<string> {
|
||||||
bucketKey: string;
|
|
||||||
metadataLocation?: string;
|
|
||||||
}> {
|
|
||||||
let projectTarballPath;
|
let projectTarballPath;
|
||||||
try {
|
try {
|
||||||
return await withAnalyticsAsync(
|
return await withAnalyticsAsync(
|
||||||
ctx.analytics,
|
|
||||||
async () => {
|
async () => {
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
Log.log(
|
Log.log(
|
||||||
@@ -258,7 +176,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
|||||||
'https://expo.fyi/eas-build-archive'
|
'https://expo.fyi/eas-build-archive'
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
|
const projectTarball = await makeProjectTarballAsync();
|
||||||
|
|
||||||
if (projectTarball.size > 1024 * 1024 * 100) {
|
if (projectTarball.size > 1024 * 1024 * 100) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
@@ -270,48 +188,29 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
|
|
||||||
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
|
|
||||||
}
|
|
||||||
|
|
||||||
projectTarballPath = projectTarball.path;
|
projectTarballPath = projectTarball.path;
|
||||||
const [bucketKey, { metadataLocation }] = await Promise.all([
|
|
||||||
uploadFileAtPathToGCSAsync(
|
|
||||||
ctx.graphqlClient,
|
|
||||||
UploadSessionType.EasBuildGcsProjectSources,
|
|
||||||
projectTarball.path,
|
|
||||||
createProgressTracker({
|
|
||||||
total: projectTarball.size,
|
|
||||||
message: ratio =>
|
|
||||||
`Uploading to EAS Build (${formatBytes(
|
|
||||||
projectTarball.size * ratio
|
|
||||||
)} / ${formatBytes(projectTarball.size)})`,
|
|
||||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
|
||||||
})
|
|
||||||
),
|
|
||||||
uploadMetadataFileAsync<TPlatform>(projectTarball, ctx),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (metadataLocation) {
|
const { bucketKey } = await uploadFileAtPathToS3Async(
|
||||||
return { bucketKey, metadataLocation };
|
UploadSessionType.EasBuildProjectSources,
|
||||||
}
|
projectTarball.path,
|
||||||
return { bucketKey };
|
createProgressTracker({
|
||||||
|
total: projectTarball.size,
|
||||||
|
message: ratio =>
|
||||||
|
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||||
|
projectTarball.size
|
||||||
|
)})`,
|
||||||
|
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return bucketKey;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
|
||||||
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
successEvent: BuildEvent.PROJECT_UPLOAD_SUCCESS,
|
||||||
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
failureEvent: BuildEvent.PROJECT_UPLOAD_FAIL,
|
||||||
properties: ctx.analyticsEventProperties,
|
trackingCtx: ctx.trackingCtx,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
|
||||||
let errMessage = 'Failed to upload the project tarball to EAS Build';
|
|
||||||
|
|
||||||
if (err.message) {
|
|
||||||
errMessage += `\n\nReason: ${err.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new EasBuildProjectArchiveUploadError(errMessage);
|
|
||||||
} finally {
|
} finally {
|
||||||
if (projectTarballPath) {
|
if (projectTarballPath) {
|
||||||
await fs.remove(projectTarballPath);
|
await fs.remove(projectTarballPath);
|
||||||
@@ -319,41 +218,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadMetadataFileAsync<TPlatform extends Platform>(
|
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
|
||||||
projectTarball: LocalFile,
|
|
||||||
ctx: BuildContext<TPlatform>
|
|
||||||
): Promise<{ metadataLocation: string | null }> {
|
|
||||||
let projectMetadataFile: LocalFile | null = null;
|
|
||||||
try {
|
|
||||||
projectMetadataFile = await makeProjectMetadataFileAsync(projectTarball.path);
|
|
||||||
|
|
||||||
const metadataLocation = await uploadFileAtPathToGCSAsync(
|
|
||||||
ctx.graphqlClient,
|
|
||||||
UploadSessionType.EasBuildGcsProjectMetadata,
|
|
||||||
projectMetadataFile.path
|
|
||||||
);
|
|
||||||
return { metadataLocation };
|
|
||||||
} catch (err: any) {
|
|
||||||
let errMessage = 'Failed to upload metadata to EAS Build';
|
|
||||||
|
|
||||||
if (err.message) {
|
|
||||||
errMessage += `\n\nReason: ${err.message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.warn(errMessage);
|
|
||||||
return { metadataLocation: null };
|
|
||||||
} finally {
|
|
||||||
if (projectMetadataFile) {
|
|
||||||
await fs.remove(projectMetadataFile.path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendBuildRequestAsync<
|
|
||||||
TPlatform extends Platform,
|
|
||||||
Credentials,
|
|
||||||
TJob extends BuildJob,
|
|
||||||
>(
|
|
||||||
builder: Builder<TPlatform, Credentials, TJob>,
|
builder: Builder<TPlatform, Credentials, TJob>,
|
||||||
job: TJob,
|
job: TJob,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
@@ -361,7 +226,6 @@ async function sendBuildRequestAsync<
|
|||||||
): Promise<BuildFragment> {
|
): Promise<BuildFragment> {
|
||||||
const { ctx } = builder;
|
const { ctx } = builder;
|
||||||
return await withAnalyticsAsync(
|
return await withAnalyticsAsync(
|
||||||
ctx.analytics,
|
|
||||||
async () => {
|
async () => {
|
||||||
if (Log.isDebug) {
|
if (Log.isDebug) {
|
||||||
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
Log.log(`Starting ${requestedPlatformDisplayNames[job.platform]} build`);
|
||||||
@@ -381,15 +245,14 @@ async function sendBuildRequestAsync<
|
|||||||
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
attemptEvent: BuildEvent.BUILD_REQUEST_ATTEMPT,
|
||||||
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
successEvent: BuildEvent.BUILD_REQUEST_SUCCESS,
|
||||||
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
failureEvent: BuildEvent.BUILD_REQUEST_FAIL,
|
||||||
properties: ctx.analyticsEventProperties,
|
trackingCtx: ctx.trackingCtx,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MaybeBuildFragment = BuildFragment | null;
|
type MaybeBuildFragment = BuildFragment | null;
|
||||||
|
|
||||||
export async function waitForBuildEndAsync(
|
export async function waitForBuildEndAsync(
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
|
||||||
{ intervalSec = 10 } = {}
|
{ intervalSec = 10 } = {}
|
||||||
): Promise<MaybeBuildFragment[]> {
|
): Promise<MaybeBuildFragment[]> {
|
||||||
@@ -404,7 +267,7 @@ export async function waitForBuildEndAsync(
|
|||||||
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
|
spinner = ora('Waiting for builds to complete. You can press Ctrl+C to exit.').start();
|
||||||
}
|
}
|
||||||
while (true) {
|
while (true) {
|
||||||
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
|
const builds = await getBuildsSafelyAsync(buildIds);
|
||||||
const { refetch } =
|
const { refetch } =
|
||||||
builds.length === 1
|
builds.length === 1
|
||||||
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
|
||||||
@@ -416,13 +279,10 @@ export async function waitForBuildEndAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getBuildsSafelyAsync(
|
async function getBuildsSafelyAsync(buildIds: string[]): Promise<MaybeBuildFragment[]> {
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
buildIds: string[]
|
|
||||||
): Promise<MaybeBuildFragment[]> {
|
|
||||||
const promises = buildIds.map(async buildId => {
|
const promises = buildIds.map(async buildId => {
|
||||||
try {
|
try {
|
||||||
return await BuildQuery.byIdAsync(graphqlClient, buildId, { useCache: false });
|
return await BuildQuery.byIdAsync(buildId, { useCache: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Log.debug('Failed to fetch the build status', err);
|
Log.debug('Failed to fetch the build status', err);
|
||||||
return null;
|
return null;
|
||||||
@@ -442,13 +302,7 @@ const queueProgressBar = new cliProgress.SingleBar(
|
|||||||
);
|
);
|
||||||
|
|
||||||
async function handleSingleBuildProgressAsync(
|
async function handleSingleBuildProgressAsync(
|
||||||
{
|
{ build, accountName }: { build: MaybeBuildFragment; accountName: string },
|
||||||
build,
|
|
||||||
accountName,
|
|
||||||
}: {
|
|
||||||
build: MaybeBuildFragment;
|
|
||||||
accountName: string;
|
|
||||||
},
|
|
||||||
{ spinner }: { spinner: Ora }
|
{ spinner }: { spinner: Ora }
|
||||||
): Promise<BuildProgressResult> {
|
): Promise<BuildProgressResult> {
|
||||||
if (build === null) {
|
if (build === null) {
|
||||||
@@ -473,7 +327,7 @@ async function handleSingleBuildProgressAsync(
|
|||||||
spinner.succeed('Build finished');
|
spinner.succeed('Build finished');
|
||||||
return { refetch: false };
|
return { refetch: false };
|
||||||
case BuildStatus.New:
|
case BuildStatus.New:
|
||||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
|
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${chalk.underline(
|
||||||
formatAccountSubscriptionsUrl(accountName)
|
formatAccountSubscriptionsUrl(accountName)
|
||||||
)}.`;
|
)}.`;
|
||||||
break;
|
break;
|
||||||
@@ -487,20 +341,18 @@ async function handleSingleBuildProgressAsync(
|
|||||||
if (
|
if (
|
||||||
!queueProgressBarStarted &&
|
!queueProgressBarStarted &&
|
||||||
typeof build.initialQueuePosition === 'number' &&
|
typeof build.initialQueuePosition === 'number' &&
|
||||||
typeof build.queuePosition === 'number' &&
|
typeof build.queuePosition === 'number'
|
||||||
typeof build.estimatedWaitTimeLeftSeconds === 'number'
|
|
||||||
) {
|
) {
|
||||||
spinner.stopAndPersist();
|
spinner.stopAndPersist();
|
||||||
if (build.priority !== BuildPriority.High) {
|
if (build.priority !== BuildPriority.High) {
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
Log.log('Start builds sooner in the priority queue.');
|
Log.log('Start builds sooner in the priority queue.');
|
||||||
Log.log(
|
Log.log(
|
||||||
`Sign up for EAS Production or Enterprise at ${link(
|
`Sign up for EAS Production or Enterprise at ${chalk.underline(
|
||||||
formatAccountSubscriptionsUrl(accountName)
|
formatAccountSubscriptionsUrl(accountName)
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
|
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
|
||||||
queueProgressBar.start(
|
queueProgressBar.start(
|
||||||
@@ -515,7 +367,6 @@ async function handleSingleBuildProgressAsync(
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case BuildStatus.PendingCancel:
|
|
||||||
case BuildStatus.Canceled:
|
case BuildStatus.Canceled:
|
||||||
spinner.fail('Build canceled');
|
spinner.fail('Build canceled');
|
||||||
return { refetch: false };
|
return { refetch: false };
|
||||||
@@ -537,8 +388,8 @@ async function handleSingleBuildProgressAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
|
const priorityToQueueDisplayName: Record<BuildPriority, string> = {
|
||||||
[BuildPriority.Normal]: 'Free tier queue',
|
[BuildPriority.Normal]: 'queue',
|
||||||
[BuildPriority.NormalPlus]: 'Free tier queue',
|
[BuildPriority.NormalPlus]: 'queue',
|
||||||
[BuildPriority.High]: 'priority queue',
|
[BuildPriority.High]: 'priority queue',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -546,7 +397,6 @@ const statusToDisplayName: Record<BuildStatus, string> = {
|
|||||||
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
|
[BuildStatus.New]: 'waiting to enter the queue (concurrency limit reached)',
|
||||||
[BuildStatus.InQueue]: 'in queue',
|
[BuildStatus.InQueue]: 'in queue',
|
||||||
[BuildStatus.InProgress]: 'in progress',
|
[BuildStatus.InProgress]: 'in progress',
|
||||||
[BuildStatus.PendingCancel]: 'canceled',
|
|
||||||
[BuildStatus.Canceled]: 'canceled',
|
[BuildStatus.Canceled]: 'canceled',
|
||||||
[BuildStatus.Finished]: 'finished',
|
[BuildStatus.Finished]: 'finished',
|
||||||
[BuildStatus.Errored]: 'failed',
|
[BuildStatus.Errored]: 'failed',
|
||||||
@@ -565,12 +415,7 @@ async function handleMultipleBuildsProgressAsync(
|
|||||||
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
|
builds.filter(build => build.status === BuildStatus.Finished).length === buildCount;
|
||||||
const allSettled =
|
const allSettled =
|
||||||
builds.filter(build =>
|
builds.filter(build =>
|
||||||
[
|
[BuildStatus.Finished, BuildStatus.Errored, BuildStatus.Canceled].includes(build.status)
|
||||||
BuildStatus.Finished,
|
|
||||||
BuildStatus.Errored,
|
|
||||||
BuildStatus.Canceled,
|
|
||||||
BuildStatus.PendingCancel,
|
|
||||||
].includes(build.status)
|
|
||||||
).length === buildCount;
|
).length === buildCount;
|
||||||
|
|
||||||
if (allSettled) {
|
if (allSettled) {
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
|
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
import Log from '../log';
|
||||||
import Log, { learnMore } from '../log';
|
import { resolveWorkflowAsync } from '../project/workflow';
|
||||||
import { easCliVersion } from '../utils/easCli';
|
import { easCliVersion } from '../utils/easCli';
|
||||||
import { Client } from '../vcs/vcs';
|
import { getVcsClient } from '../vcs';
|
||||||
|
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||||
|
|
||||||
interface ConfigureParams {
|
interface ConfigureParams {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
nonInteractive: boolean;
|
nonInteractive: boolean;
|
||||||
vcsClient: Client;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function easJsonExistsAsync(projectDir: string): Promise<boolean> {
|
|
||||||
return await fs.pathExists(EasJsonAccessor.formatEasJsonPath(projectDir));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,7 +24,7 @@ export async function easJsonExistsAsync(projectDir: string): Promise<boolean> {
|
|||||||
export async function ensureProjectConfiguredAsync(
|
export async function ensureProjectConfiguredAsync(
|
||||||
configureParams: ConfigureParams
|
configureParams: ConfigureParams
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (await easJsonExistsAsync(configureParams.projectDir)) {
|
if (await fs.pathExists(EasJsonAccessor.formatEasJsonPath(configureParams.projectDir))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,24 +32,20 @@ export async function ensureProjectConfiguredAsync(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function configureAsync({
|
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
|
||||||
projectDir,
|
await maybeBailOnRepoStatusAsync();
|
||||||
nonInteractive,
|
|
||||||
vcsClient,
|
|
||||||
}: ConfigureParams): Promise<void> {
|
|
||||||
await maybeBailOnRepoStatusAsync(vcsClient, nonInteractive);
|
|
||||||
|
|
||||||
await createEasJsonAsync(projectDir, vcsClient);
|
await createEasJsonAsync(projectDir);
|
||||||
|
|
||||||
if (await vcsClient.isCommitRequiredAsync()) {
|
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
await reviewAndCommitChangesAsync(vcsClient, 'Configure EAS Build', {
|
await reviewAndCommitChangesAsync('Configure EAS Build', {
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EAS_JSON_DEFAULT: EasJson = {
|
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
|
||||||
cli: {
|
cli: {
|
||||||
version: `>= ${easCliVersion}`,
|
version: `>= ${easCliVersion}`,
|
||||||
},
|
},
|
||||||
@@ -71,14 +64,43 @@ const EAS_JSON_DEFAULT: EasJson = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function createEasJsonAsync(projectDir: string, vcsClient: Client): Promise<void> {
|
const EAS_JSON_BARE_DEFAULT: EasJson = {
|
||||||
|
cli: {
|
||||||
|
version: `>= ${easCliVersion}`,
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
development: {
|
||||||
|
distribution: 'internal',
|
||||||
|
android: {
|
||||||
|
gradleCommand: ':app:assembleDebug',
|
||||||
|
},
|
||||||
|
ios: {
|
||||||
|
buildConfiguration: 'Debug',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
distribution: 'internal',
|
||||||
|
},
|
||||||
|
production: {},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
production: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createEasJsonAsync(projectDir: string): Promise<void> {
|
||||||
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
|
||||||
|
|
||||||
await fs.writeFile(easJsonPath, `${JSON.stringify(EAS_JSON_DEFAULT, null, 2)}\n`);
|
const hasAndroidNativeProject =
|
||||||
await vcsClient.trackFileAsync(easJsonPath);
|
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
|
||||||
Log.withTick(
|
const hasIosNativeProject =
|
||||||
`Generated ${chalk.bold('eas.json')}. ${learnMore(
|
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
|
||||||
'https://docs.expo.dev/build-reference/eas-json/'
|
const easJson =
|
||||||
)}`
|
hasAndroidNativeProject || hasIosNativeProject
|
||||||
);
|
? EAS_JSON_BARE_DEFAULT
|
||||||
|
: EAS_JSON_MANAGED_DEFAULT;
|
||||||
|
|
||||||
|
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
|
||||||
|
await getVcsClient().trackFileAsync(easJsonPath);
|
||||||
|
Log.withTick(`Generated ${chalk.bold('eas.json')}`);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,15 @@
|
|||||||
import { ExpoConfig } from '@expo/config';
|
import { ExpoConfig } from '@expo/config';
|
||||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { BuildProfile, EasJson } from '@expo/eas-json';
|
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||||
import { LoggerLevel } from '@expo/logger';
|
|
||||||
import { NodePackageManager } from '@expo/package-manager';
|
|
||||||
|
|
||||||
import { LocalBuildOptions } from './local';
|
import { TrackingContext } from '../analytics/common';
|
||||||
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
|
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { CredentialsContext } from '../credentials/context';
|
import { CredentialsContext } from '../credentials/context';
|
||||||
import { Target } from '../credentials/ios/types';
|
import { Target } from '../credentials/ios/types';
|
||||||
import { BuildResourceClass } from '../graphql/generated';
|
import { BuildResourceClass } from '../graphql/generated';
|
||||||
import { GradleBuildContext } from '../project/android/gradle';
|
import { GradleBuildContext } from '../project/android/gradle';
|
||||||
import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
|
|
||||||
import { XcodeBuildContext } from '../project/ios/scheme';
|
import { XcodeBuildContext } from '../project/ios/scheme';
|
||||||
import { Actor } from '../user/User';
|
import { Actor } from '../user/User';
|
||||||
import { Client } from '../vcs/vcs';
|
import { LocalBuildOptions } from './local';
|
||||||
|
|
||||||
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
|
||||||
|
|
||||||
@@ -50,17 +45,9 @@ export interface BuildContext<T extends Platform> {
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
analyticsEventProperties: AnalyticsEventProperties;
|
trackingCtx: TrackingContext;
|
||||||
user: Actor;
|
user: Actor;
|
||||||
graphqlClient: ExpoGraphqlClient;
|
|
||||||
analytics: Analytics;
|
|
||||||
workflow: Workflow;
|
workflow: Workflow;
|
||||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
|
||||||
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
|
||||||
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
ios: T extends Platform.IOS ? IosBuildContext : undefined;
|
||||||
developmentClient: boolean;
|
|
||||||
requiredPackageManager: NodePackageManager['name'] | null;
|
|
||||||
vcsClient: Client;
|
|
||||||
loggerLevel?: LoggerLevel;
|
|
||||||
repack: boolean;
|
|
||||||
}
|
}
|
||||||
@@ -1,26 +1,22 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
import { Platform } from '@expo/eas-build-job';
|
||||||
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
|
import { BuildProfile, EasJson } from '@expo/eas-json';
|
||||||
import JsonFile from '@expo/json-file';
|
import JsonFile from '@expo/json-file';
|
||||||
import { LoggerLevel } from '@expo/logger';
|
|
||||||
import { resolvePackageManager } from '@expo/package-manager';
|
|
||||||
import getenv from 'getenv';
|
import getenv from 'getenv';
|
||||||
import resolveFrom from 'resolve-from';
|
import resolveFrom from 'resolve-from';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { TrackingContext } from '../analytics/common';
|
||||||
|
import { Analytics, BuildEvent } from '../analytics/events';
|
||||||
|
import { CredentialsContext } from '../credentials/context';
|
||||||
|
import { BuildResourceClass } from '../graphql/generated';
|
||||||
|
import { getExpoConfig } from '../project/expoConfig';
|
||||||
|
import { getOwnerAccountForProjectIdAsync, getProjectIdAsync } from '../project/projectUtils';
|
||||||
|
import { resolveWorkflowAsync } from '../project/workflow';
|
||||||
|
import { ensureLoggedInAsync } from '../user/actions';
|
||||||
import { createAndroidContextAsync } from './android/build';
|
import { createAndroidContextAsync } from './android/build';
|
||||||
import { BuildContext, CommonContext } from './context';
|
import { BuildContext, CommonContext } from './context';
|
||||||
import { createIosContextAsync } from './ios/build';
|
import { createIosContextAsync } from './ios/build';
|
||||||
import { LocalBuildOptions } from './local';
|
import { LocalBuildOptions } from './local';
|
||||||
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
|
|
||||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
|
||||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { CredentialsContext } from '../credentials/context';
|
|
||||||
import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
|
|
||||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
|
||||||
import { resolveWorkflowAsync } from '../project/workflow';
|
|
||||||
import { Actor } from '../user/User';
|
|
||||||
import { Client } from '../vcs/vcs';
|
|
||||||
|
|
||||||
export async function createBuildContextAsync<T extends Platform>({
|
export async function createBuildContextAsync<T extends Platform>({
|
||||||
buildProfileName,
|
buildProfileName,
|
||||||
@@ -32,17 +28,8 @@ export async function createBuildContextAsync<T extends Platform>({
|
|||||||
noWait,
|
noWait,
|
||||||
platform,
|
platform,
|
||||||
projectDir,
|
projectDir,
|
||||||
resourceClassFlag,
|
resourceClass,
|
||||||
message,
|
message,
|
||||||
actor,
|
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
vcsClient,
|
|
||||||
getDynamicPrivateProjectConfigAsync,
|
|
||||||
customBuildConfigMetadata,
|
|
||||||
buildLoggerLevel,
|
|
||||||
freezeCredentials,
|
|
||||||
repack,
|
|
||||||
}: {
|
}: {
|
||||||
buildProfileName: string;
|
buildProfileName: string;
|
||||||
buildProfile: BuildProfile<T>;
|
buildProfile: BuildProfile<T>;
|
||||||
@@ -53,44 +40,29 @@ export async function createBuildContextAsync<T extends Platform>({
|
|||||||
noWait: boolean;
|
noWait: boolean;
|
||||||
platform: T;
|
platform: T;
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
resourceClassFlag?: ResourceClass;
|
resourceClass: BuildResourceClass;
|
||||||
message?: string;
|
message?: string;
|
||||||
actor: Actor;
|
|
||||||
graphqlClient: ExpoGraphqlClient;
|
|
||||||
analytics: Analytics;
|
|
||||||
vcsClient: Client;
|
|
||||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
|
||||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
|
||||||
buildLoggerLevel?: LoggerLevel;
|
|
||||||
freezeCredentials: boolean;
|
|
||||||
repack: boolean;
|
|
||||||
}): Promise<BuildContext<T>> {
|
}): Promise<BuildContext<T>> {
|
||||||
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
|
const exp = getExpoConfig(projectDir, { env: buildProfile.env });
|
||||||
|
|
||||||
|
const user = await ensureLoggedInAsync({ nonInteractive });
|
||||||
const projectName = exp.slug;
|
const projectName = exp.slug;
|
||||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
const projectId = await getProjectIdAsync(exp, {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
|
env: buildProfile.env,
|
||||||
|
nonInteractive,
|
||||||
|
});
|
||||||
|
const account = await getOwnerAccountForProjectIdAsync(projectId);
|
||||||
|
const workflow = await resolveWorkflowAsync(projectDir, platform);
|
||||||
const accountId = account.id;
|
const accountId = account.id;
|
||||||
const runFromCI = getenv.boolish('CI', false);
|
const runFromCI = getenv.boolish('CI', false);
|
||||||
const developmentClient =
|
|
||||||
buildProfile.developmentClient ??
|
|
||||||
(platform === Platform.ANDROID
|
|
||||||
? (buildProfile as BuildProfile<Platform.ANDROID>)?.gradleCommand === ':app:assembleDebug'
|
|
||||||
: (buildProfile as BuildProfile<Platform.IOS>)?.buildConfiguration === 'Debug') ??
|
|
||||||
false;
|
|
||||||
|
|
||||||
const requiredPackageManager = resolvePackageManager(projectDir);
|
|
||||||
|
|
||||||
const credentialsCtx = new CredentialsContext({
|
const credentialsCtx = new CredentialsContext({
|
||||||
projectInfo: { exp, projectId },
|
exp,
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
projectDir,
|
projectDir,
|
||||||
user: actor,
|
user,
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
env: buildProfile.env,
|
env: buildProfile.env,
|
||||||
easJsonCliConfig,
|
easJsonCliConfig,
|
||||||
vcsClient,
|
|
||||||
freezeCredentials,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const devClientProperties = getDevClientEventProperties({
|
const devClientProperties = getDevClientEventProperties({
|
||||||
@@ -98,10 +70,9 @@ export async function createBuildContextAsync<T extends Platform>({
|
|||||||
projectDir,
|
projectDir,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
});
|
});
|
||||||
const analyticsEventProperties = {
|
const trackingCtx = {
|
||||||
tracking_id: uuidv4(),
|
tracking_id: uuidv4(),
|
||||||
platform,
|
platform,
|
||||||
...(exp.sdkVersion && { sdk_version: exp.sdkVersion }),
|
|
||||||
...(accountId && { account_id: accountId }),
|
...(accountId && { account_id: accountId }),
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
project_type: workflow,
|
project_type: workflow,
|
||||||
@@ -109,13 +80,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
|||||||
no_wait: noWait,
|
no_wait: noWait,
|
||||||
run_from_ci: runFromCI,
|
run_from_ci: runFromCI,
|
||||||
};
|
};
|
||||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
Analytics.logEvent(BuildEvent.BUILD_COMMAND, trackingCtx);
|
||||||
|
|
||||||
const resourceClass = await resolveBuildResourceClassAsync(
|
|
||||||
buildProfile,
|
|
||||||
platform,
|
|
||||||
resourceClassFlag
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonContext: CommonContext<T> = {
|
const commonContext: CommonContext<T> = {
|
||||||
accountName: account.name,
|
accountName: account.name,
|
||||||
@@ -133,19 +98,11 @@ export async function createBuildContextAsync<T extends Platform>({
|
|||||||
projectDir,
|
projectDir,
|
||||||
projectId,
|
projectId,
|
||||||
projectName,
|
projectName,
|
||||||
analyticsEventProperties,
|
trackingCtx,
|
||||||
user: actor,
|
user,
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
vcsClient,
|
|
||||||
workflow,
|
workflow,
|
||||||
message,
|
message,
|
||||||
runFromCI,
|
runFromCI,
|
||||||
customBuildConfigMetadata,
|
|
||||||
developmentClient,
|
|
||||||
requiredPackageManager,
|
|
||||||
loggerLevel: buildLoggerLevel,
|
|
||||||
repack,
|
|
||||||
};
|
};
|
||||||
if (platform === Platform.ANDROID) {
|
if (platform === Platform.ANDROID) {
|
||||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||||
@@ -170,7 +127,7 @@ function getDevClientEventProperties({
|
|||||||
platform: Platform;
|
platform: Platform;
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
buildProfile: BuildProfile;
|
buildProfile: BuildProfile;
|
||||||
}): Partial<AnalyticsEventProperties> {
|
}): Partial<TrackingContext> {
|
||||||
let includesDevClient;
|
let includesDevClient;
|
||||||
const version = tryGetDevClientVersion(projectDir);
|
const version = tryGetDevClientVersion(projectDir);
|
||||||
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
if (platform === Platform.ANDROID && 'gradleCommand' in buildProfile) {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { EasCommandError } from '../commandUtils/errors';
|
|
||||||
|
|
||||||
export class TurtleDeprecatedJobFormatError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildFreeTierDisabledError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildFreeTierDisabledIOSError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildFreeTierDisabledAndroidError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildFreeTierLimitExceededError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildFreeTierIosLimitExceededError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildResourceClassNotAvailableInFreeTierError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildLegacyResourceClassNotAvailableError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class RequestValidationError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildDownForMaintenanceError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildTooManyPendingBuildsError extends EasCommandError {}
|
|
||||||
|
|
||||||
export class EasBuildProjectArchiveUploadError extends EasCommandError {}
|
|
||||||
@@ -1,13 +1,4 @@
|
|||||||
import {
|
import { ArchiveSource, ArchiveSourceType, Metadata, Workflow } from '@expo/eas-build-job';
|
||||||
ArchiveSource,
|
|
||||||
ArchiveSourceType,
|
|
||||||
BuildMode,
|
|
||||||
BuildTrigger,
|
|
||||||
Metadata,
|
|
||||||
Workflow,
|
|
||||||
} from '@expo/eas-build-job';
|
|
||||||
import { LoggerLevel } from '@expo/logger';
|
|
||||||
import assert from 'assert';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BuildCredentialsSource,
|
BuildCredentialsSource,
|
||||||
@@ -15,19 +6,15 @@ import {
|
|||||||
BuildMetadataInput,
|
BuildMetadataInput,
|
||||||
BuildWorkflow,
|
BuildWorkflow,
|
||||||
DistributionType,
|
DistributionType,
|
||||||
BuildMode as GraphQLBuildMode,
|
|
||||||
BuildTrigger as GraphQLBuildTrigger,
|
|
||||||
ProjectArchiveSourceInput,
|
ProjectArchiveSourceInput,
|
||||||
ProjectArchiveSourceType,
|
ProjectArchiveSourceType,
|
||||||
WorkerLoggerLevel,
|
|
||||||
} from '../graphql/generated';
|
} from '../graphql/generated';
|
||||||
|
|
||||||
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
|
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
|
||||||
if (archiveSource.type === ArchiveSourceType.GCS) {
|
if (archiveSource.type === ArchiveSourceType.S3) {
|
||||||
return {
|
return {
|
||||||
type: ProjectArchiveSourceType.Gcs,
|
type: ProjectArchiveSourceType.S3,
|
||||||
bucketKey: archiveSource.bucketKey,
|
bucketKey: archiveSource.bucketKey,
|
||||||
metadataLocation: archiveSource.metadataLocation,
|
|
||||||
};
|
};
|
||||||
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
} else if (archiveSource.type === ArchiveSourceType.URL) {
|
||||||
return {
|
return {
|
||||||
@@ -89,34 +76,3 @@ export function transformIosEnterpriseProvisioning(
|
|||||||
return BuildIosEnterpriseProvisioning.Universal;
|
return BuildIosEnterpriseProvisioning.Universal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildModeToGraphQLBuildMode: Record<BuildMode, GraphQLBuildMode> = {
|
|
||||||
[BuildMode.BUILD]: GraphQLBuildMode.Build,
|
|
||||||
[BuildMode.CUSTOM]: GraphQLBuildMode.Custom,
|
|
||||||
[BuildMode.RESIGN]: GraphQLBuildMode.Resign,
|
|
||||||
[BuildMode.REPACK]: GraphQLBuildMode.Repack,
|
|
||||||
};
|
|
||||||
|
|
||||||
export function transformBuildMode(buildMode: BuildMode): GraphQLBuildMode {
|
|
||||||
const graphQLBuildMode = buildModeToGraphQLBuildMode[buildMode];
|
|
||||||
assert(graphQLBuildMode, `Unsupported build mode: ${buildMode}`);
|
|
||||||
return graphQLBuildMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loggerLevelToGraphQLWorkerLoggerLevel: Record<LoggerLevel, WorkerLoggerLevel> = {
|
|
||||||
[LoggerLevel.TRACE]: WorkerLoggerLevel.Trace,
|
|
||||||
[LoggerLevel.DEBUG]: WorkerLoggerLevel.Debug,
|
|
||||||
[LoggerLevel.INFO]: WorkerLoggerLevel.Info,
|
|
||||||
[LoggerLevel.WARN]: WorkerLoggerLevel.Warn,
|
|
||||||
[LoggerLevel.ERROR]: WorkerLoggerLevel.Error,
|
|
||||||
[LoggerLevel.FATAL]: WorkerLoggerLevel.Fatal,
|
|
||||||
};
|
|
||||||
@@ -6,7 +6,6 @@ import os from 'os';
|
|||||||
import type { XCBuildConfiguration } from 'xcode';
|
import type { XCBuildConfiguration } from 'xcode';
|
||||||
|
|
||||||
import { readPlistAsync } from '../../../utils/plist';
|
import { readPlistAsync } from '../../../utils/plist';
|
||||||
import { resolveVcsClient } from '../../../vcs';
|
|
||||||
import {
|
import {
|
||||||
BumpStrategy,
|
BumpStrategy,
|
||||||
bumpVersionAsync,
|
bumpVersionAsync,
|
||||||
@@ -19,8 +18,6 @@ import {
|
|||||||
|
|
||||||
jest.mock('fs');
|
jest.mock('fs');
|
||||||
|
|
||||||
const vcsClient = resolveVcsClient();
|
|
||||||
|
|
||||||
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
|
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
|
||||||
IOSConfig.Target,
|
IOSConfig.Target,
|
||||||
'getXCBuildConfigurationFromPbxproj'
|
'getXCBuildConfigurationFromPbxproj'
|
||||||
@@ -62,7 +59,7 @@ describe(evaluateTemplateString, () => {
|
|||||||
// bare workflow
|
// bare workflow
|
||||||
describe(bumpVersionAsync, () => {
|
describe(bumpVersionAsync, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getPbxproj.mockImplementation((): any => {});
|
getPbxproj.mockImplementation(() => ({} as any));
|
||||||
});
|
});
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
getXCBuildConfigurationFromPbxproj.mockReset();
|
getXCBuildConfigurationFromPbxproj.mockReset();
|
||||||
@@ -76,11 +73,11 @@ describe(bumpVersionAsync, () => {
|
|||||||
buildSettings: {
|
buildSettings: {
|
||||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||||
},
|
},
|
||||||
}) as XCBuildConfiguration
|
} as XCBuildConfiguration)
|
||||||
);
|
);
|
||||||
await bumpVersionAsync({
|
await bumpVersionAsync({
|
||||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
@@ -91,9 +88,9 @@ describe(bumpVersionAsync, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
const infoPlist = (await readPlistAsync(
|
const infoPlist = (await readPlistAsync(
|
||||||
'/app/ios/myproject/Info.plist'
|
'/repo/ios/myproject/Info.plist'
|
||||||
)) as IOSConfig.InfoPlist;
|
)) as IOSConfig.InfoPlist;
|
||||||
expect(fakeExp.version).toBe('1.0.0');
|
expect(fakeExp.version).toBe('1.0.0');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||||
@@ -112,11 +109,11 @@ describe(bumpVersionAsync, () => {
|
|||||||
buildSettings: {
|
buildSettings: {
|
||||||
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
|
||||||
},
|
},
|
||||||
}) as XCBuildConfiguration
|
} as XCBuildConfiguration)
|
||||||
);
|
);
|
||||||
await bumpVersionAsync({
|
await bumpVersionAsync({
|
||||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
@@ -127,9 +124,9 @@ describe(bumpVersionAsync, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
const infoPlist = (await readPlistAsync(
|
const infoPlist = (await readPlistAsync(
|
||||||
'/app/ios/myproject/Info2.plist'
|
'/repo/ios/myproject/Info2.plist'
|
||||||
)) as IOSConfig.InfoPlist;
|
)) as IOSConfig.InfoPlist;
|
||||||
expect(fakeExp.version).toBe('1.0.0');
|
expect(fakeExp.version).toBe('1.0.0');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||||
@@ -147,11 +144,11 @@ describe(bumpVersionAsync, () => {
|
|||||||
buildSettings: {
|
buildSettings: {
|
||||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||||
},
|
},
|
||||||
}) as XCBuildConfiguration
|
} as XCBuildConfiguration)
|
||||||
);
|
);
|
||||||
await bumpVersionAsync({
|
await bumpVersionAsync({
|
||||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
@@ -162,9 +159,9 @@ describe(bumpVersionAsync, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
const infoPlist = (await readPlistAsync(
|
const infoPlist = (await readPlistAsync(
|
||||||
'/app/ios/myproject/Info.plist'
|
'/repo/ios/myproject/Info.plist'
|
||||||
)) as IOSConfig.InfoPlist;
|
)) as IOSConfig.InfoPlist;
|
||||||
expect(fakeExp.version).toBe('1.0.1');
|
expect(fakeExp.version).toBe('1.0.1');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||||
@@ -182,11 +179,11 @@ describe(bumpVersionAsync, () => {
|
|||||||
buildSettings: {
|
buildSettings: {
|
||||||
INFOPLIST_FILE: 'myproject/Info.plist',
|
INFOPLIST_FILE: 'myproject/Info.plist',
|
||||||
},
|
},
|
||||||
}) as XCBuildConfiguration
|
} as XCBuildConfiguration)
|
||||||
);
|
);
|
||||||
await bumpVersionAsync({
|
await bumpVersionAsync({
|
||||||
bumpStrategy: BumpStrategy.NOOP,
|
bumpStrategy: BumpStrategy.NOOP,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
@@ -197,9 +194,9 @@ describe(bumpVersionAsync, () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
const infoPlist = (await readPlistAsync(
|
const infoPlist = (await readPlistAsync(
|
||||||
'/app/ios/myproject/Info.plist'
|
'/repo/ios/myproject/Info.plist'
|
||||||
)) as IOSConfig.InfoPlist;
|
)) as IOSConfig.InfoPlist;
|
||||||
expect(fakeExp.version).toBe('1.0.0');
|
expect(fakeExp.version).toBe('1.0.0');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||||
@@ -217,11 +214,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
|||||||
|
|
||||||
await bumpVersionInAppJsonAsync({
|
await bumpVersionInAppJsonAsync({
|
||||||
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
bumpStrategy: BumpStrategy.BUILD_NUMBER,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
expect(fakeExp.version).toBe('1.0.0');
|
expect(fakeExp.version).toBe('1.0.0');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('2');
|
expect(fakeExp.ios?.buildNumber).toBe('2');
|
||||||
expect(appJSON.expo.version).toBe('1.0.0');
|
expect(appJSON.expo.version).toBe('1.0.0');
|
||||||
@@ -233,11 +230,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
|||||||
|
|
||||||
await bumpVersionInAppJsonAsync({
|
await bumpVersionInAppJsonAsync({
|
||||||
bumpStrategy: BumpStrategy.APP_VERSION,
|
bumpStrategy: BumpStrategy.APP_VERSION,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
expect(fakeExp.version).toBe('1.0.1');
|
expect(fakeExp.version).toBe('1.0.1');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||||
expect(appJSON.expo.version).toBe('1.0.1');
|
expect(appJSON.expo.version).toBe('1.0.1');
|
||||||
@@ -249,11 +246,11 @@ describe(bumpVersionInAppJsonAsync, () => {
|
|||||||
|
|
||||||
await bumpVersionInAppJsonAsync({
|
await bumpVersionInAppJsonAsync({
|
||||||
bumpStrategy: BumpStrategy.NOOP,
|
bumpStrategy: BumpStrategy.NOOP,
|
||||||
projectDir: '/app',
|
projectDir: '/repo',
|
||||||
exp: fakeExp,
|
exp: fakeExp,
|
||||||
});
|
});
|
||||||
|
|
||||||
const appJSON = await fs.readJSON('/app/app.json');
|
const appJSON = await fs.readJSON('/repo/app.json');
|
||||||
expect(fakeExp.version).toBe('1.0.0');
|
expect(fakeExp.version).toBe('1.0.0');
|
||||||
expect(fakeExp.ios?.buildNumber).toBe('1');
|
expect(fakeExp.ios?.buildNumber).toBe('1');
|
||||||
expect(appJSON.expo.version).toBe('1.0.0');
|
expect(appJSON.expo.version).toBe('1.0.0');
|
||||||
@@ -265,7 +262,7 @@ describe(readBuildNumberAsync, () => {
|
|||||||
describe('bare project', () => {
|
describe('bare project', () => {
|
||||||
it('reads the build number from native code', async () => {
|
it('reads the build number from native code', async () => {
|
||||||
const exp = initBareWorkflowProject();
|
const exp = initBareWorkflowProject();
|
||||||
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
|
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||||
expect(buildNumber).toBe('1');
|
expect(buildNumber).toBe('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -273,7 +270,7 @@ describe(readBuildNumberAsync, () => {
|
|||||||
describe('managed project', () => {
|
describe('managed project', () => {
|
||||||
it('reads the build number from expo config', async () => {
|
it('reads the build number from expo config', async () => {
|
||||||
const exp = initManagedProject();
|
const exp = initManagedProject();
|
||||||
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
|
const buildNumber = await readBuildNumberAsync('/repo', exp, {});
|
||||||
expect(buildNumber).toBe('1');
|
expect(buildNumber).toBe('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -283,48 +280,24 @@ describe(readShortVersionAsync, () => {
|
|||||||
describe('bare project', () => {
|
describe('bare project', () => {
|
||||||
it('reads the short version from native code', async () => {
|
it('reads the short version from native code', async () => {
|
||||||
const exp = initBareWorkflowProject();
|
const exp = initBareWorkflowProject();
|
||||||
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
|
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||||
expect(appVersion).toBe('1.0.0');
|
expect(appVersion).toBe('1.0.0');
|
||||||
});
|
});
|
||||||
it('evaluates interpolated build number', async () => {
|
it('evaluates interpolated build number', async () => {
|
||||||
const exp = initBareWorkflowProject({
|
const exp = initBareWorkflowProject({
|
||||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
||||||
});
|
});
|
||||||
const buildNumber = await readShortVersionAsync(
|
const buildNumber = await readShortVersionAsync('/repo', exp, {
|
||||||
'/app',
|
CURRENT_PROJECT_VERSION: '1.0.0',
|
||||||
exp,
|
|
||||||
{
|
|
||||||
CURRENT_PROJECT_VERSION: '1.0.0',
|
|
||||||
},
|
|
||||||
vcsClient
|
|
||||||
);
|
|
||||||
expect(buildNumber).toBe('1.0.0');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('fails when build number is invalid', async () => {
|
|
||||||
const exp = initBareWorkflowProject({
|
|
||||||
appVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
||||||
});
|
});
|
||||||
|
expect(buildNumber).toBe('1.0.0');
|
||||||
await expect(
|
|
||||||
readShortVersionAsync(
|
|
||||||
'/app',
|
|
||||||
exp,
|
|
||||||
{
|
|
||||||
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
|
|
||||||
},
|
|
||||||
vcsClient
|
|
||||||
)
|
|
||||||
).rejects.toThrowError(
|
|
||||||
'CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: 0.0.7.1.028'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('managed project', () => {
|
describe('managed project', () => {
|
||||||
it('reads the version from app config', async () => {
|
it('reads the version from app config', async () => {
|
||||||
const exp = initBareWorkflowProject();
|
const exp = initBareWorkflowProject();
|
||||||
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
|
const appVersion = await readShortVersionAsync('/repo', exp, {});
|
||||||
expect(appVersion).toBe('1.0.0');
|
expect(appVersion).toBe('1.0.0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -336,75 +309,66 @@ describe(getInfoPlistPath, () => {
|
|||||||
{
|
{
|
||||||
'./ios/testapp/Info.plist': '',
|
'./ios/testapp/Info.plist': '',
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
const plistPath = getInfoPlistPath('/app', {});
|
const plistPath = getInfoPlistPath('/repo', {});
|
||||||
expect(plistPath).toBe('/app/ios/testapp/Info.plist');
|
expect(plistPath).toBe('/repo/ios/testapp/Info.plist');
|
||||||
});
|
});
|
||||||
it('returns INFOPLIST_FILE if specified', () => {
|
it('returns INFOPLIST_FILE if specified', () => {
|
||||||
vol.fromJSON(
|
vol.fromJSON(
|
||||||
{
|
{
|
||||||
'./ios/testapp/Info.plist': '',
|
'./ios/testapp/Info.plist': '',
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
const plistPath = getInfoPlistPath('/app', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
|
const plistPath = getInfoPlistPath('/repo', { INFOPLIST_FILE: './qwert/NotInfo.plist' });
|
||||||
expect(plistPath).toBe('/app/ios/qwert/NotInfo.plist');
|
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||||
});
|
});
|
||||||
it('evaluates SRCROOT in Info.plist', () => {
|
it('evaluates SRCROOT in Info.plist', () => {
|
||||||
vol.fromJSON(
|
vol.fromJSON(
|
||||||
{
|
{
|
||||||
'./ios/testapp/Info.plist': '',
|
'./ios/testapp/Info.plist': '',
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
const plistPath = getInfoPlistPath('/app', {
|
const plistPath = getInfoPlistPath('/repo', {
|
||||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
|
INFOPLIST_FILE: '$(SRCROOT)/qwert/NotInfo.plist',
|
||||||
});
|
});
|
||||||
expect(plistPath).toBe('/app/ios/qwert/NotInfo.plist');
|
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||||
});
|
});
|
||||||
it('evaluates BuildSettings in Info.plist', () => {
|
it('evaluates BuildSettings in Info.plist', () => {
|
||||||
vol.fromJSON(
|
vol.fromJSON(
|
||||||
{
|
{
|
||||||
'./ios/testapp/Info.plist': '',
|
'./ios/testapp/Info.plist': '',
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
const plistPath = getInfoPlistPath('/app', {
|
const plistPath = getInfoPlistPath('/repo', {
|
||||||
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
|
INFOPLIST_FILE: '$(SRCROOT)/qwert/$(TARGET_NAME).plist',
|
||||||
TARGET_NAME: 'NotInfo',
|
TARGET_NAME: 'NotInfo',
|
||||||
});
|
});
|
||||||
expect(plistPath).toBe('/app/ios/qwert/NotInfo.plist');
|
expect(plistPath).toBe('/repo/ios/qwert/NotInfo.plist');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function initBareWorkflowProject({
|
function initBareWorkflowProject({
|
||||||
appVersion = '1.0.0',
|
appVersion = '1.0.0',
|
||||||
buildNumber = '1',
|
buildNumber = '1',
|
||||||
expoConfig = {},
|
|
||||||
infoPlistName = 'Info.plist',
|
infoPlistName = 'Info.plist',
|
||||||
}: {
|
}: { appVersion?: string; buildNumber?: string; infoPlistName?: string } = {}): ExpoConfig {
|
||||||
appVersion?: string;
|
|
||||||
buildNumber?: string;
|
|
||||||
infoPlistName?: string;
|
|
||||||
expoConfig?: { version?: string; ios?: object };
|
|
||||||
} = {}): ExpoConfig {
|
|
||||||
const fakeExp: ExpoConfig = {
|
const fakeExp: ExpoConfig = {
|
||||||
name: 'myproject',
|
name: 'myproject',
|
||||||
slug: 'myproject',
|
slug: 'myproject',
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
ios: {
|
ios: {
|
||||||
buildNumber: '1',
|
buildNumber: '1',
|
||||||
...expoConfig.ios,
|
|
||||||
},
|
},
|
||||||
...expoConfig,
|
|
||||||
};
|
};
|
||||||
vol.fromJSON(
|
vol.fromJSON(
|
||||||
{
|
{
|
||||||
'./app.json': JSON.stringify({
|
'./app.json': JSON.stringify({
|
||||||
expo: fakeExp,
|
expo: fakeExp,
|
||||||
}),
|
}),
|
||||||
'./ios/myproject.xcodeproj/project.pbxproj': '',
|
|
||||||
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
|
[`./ios/myproject/${infoPlistName}`]: `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
@@ -416,7 +380,7 @@ function initBareWorkflowProject({
|
|||||||
</dict>
|
</dict>
|
||||||
</plist>`,
|
</plist>`,
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
|
|
||||||
return fakeExp;
|
return fakeExp;
|
||||||
@@ -438,7 +402,7 @@ function initManagedProject(): ExpoConfig {
|
|||||||
expo: fakeExp,
|
expo: fakeExp,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'/app'
|
'/repo'
|
||||||
);
|
);
|
||||||
|
|
||||||
return fakeExp;
|
return fakeExp;
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { AppVersionSource } from '@expo/eas-json';
|
import { AppVersionSource } from '@expo/eas-json';
|
||||||
|
|
||||||
import { ensureIosCredentialsAsync } from './credentials';
|
|
||||||
import { transformJob } from './graphql';
|
|
||||||
import { prepareJobAsync } from './prepareJob';
|
|
||||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
|
||||||
import { resolveRemoteBuildNumberAsync } from './version';
|
|
||||||
import { IosCredentials } from '../../credentials/ios/types';
|
import { IosCredentials } from '../../credentials/ios/types';
|
||||||
import { BuildParamsInput } from '../../graphql/generated';
|
import { BuildParamsInput } from '../../graphql/generated';
|
||||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||||
@@ -15,11 +10,12 @@ import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/ta
|
|||||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||||
import { transformMetadata } from '../graphql';
|
import { transformMetadata } from '../graphql';
|
||||||
import {
|
import { checkGoogleServicesFileAsync, checkNodeEnvVariable } from '../validate';
|
||||||
checkGoogleServicesFileAsync,
|
import { ensureIosCredentialsAsync } from './credentials';
|
||||||
checkNodeEnvVariable,
|
import { transformJob } from './graphql';
|
||||||
validatePNGsForManagedProjectAsync,
|
import { prepareJobAsync } from './prepareJob';
|
||||||
} from '../validate';
|
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||||
|
import { resolveRemoteBuildNumberAsync } from './version';
|
||||||
|
|
||||||
export async function createIosContextAsync(
|
export async function createIosContextAsync(
|
||||||
ctx: CommonContext<Platform.IOS>
|
ctx: CommonContext<Platform.IOS>
|
||||||
@@ -27,19 +23,17 @@ export async function createIosContextAsync(
|
|||||||
const { buildProfile } = ctx;
|
const { buildProfile } = ctx;
|
||||||
|
|
||||||
if (ctx.workflow === Workflow.MANAGED) {
|
if (ctx.workflow === Workflow.MANAGED) {
|
||||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
|
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx.projectDir, ctx.exp);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkNodeEnvVariable(ctx);
|
checkNodeEnvVariable(ctx);
|
||||||
await checkGoogleServicesFileAsync(ctx);
|
await checkGoogleServicesFileAsync(ctx);
|
||||||
await validatePNGsForManagedProjectAsync(ctx);
|
|
||||||
|
|
||||||
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
|
||||||
{
|
{
|
||||||
projectDir: ctx.projectDir,
|
projectDir: ctx.projectDir,
|
||||||
nonInteractive: ctx.nonInteractive,
|
nonInteractive: ctx.nonInteractive,
|
||||||
exp: ctx.exp,
|
exp: ctx.exp,
|
||||||
vcsClient: ctx.vcsClient,
|
|
||||||
},
|
},
|
||||||
buildProfile
|
buildProfile
|
||||||
);
|
);
|
||||||
@@ -48,18 +42,16 @@ export async function createIosContextAsync(
|
|||||||
exp: ctx.exp,
|
exp: ctx.exp,
|
||||||
xcodeBuildContext,
|
xcodeBuildContext,
|
||||||
env: buildProfile.env,
|
env: buildProfile.env,
|
||||||
vcsClient: ctx.vcsClient,
|
|
||||||
});
|
});
|
||||||
const applicationTarget = findApplicationTarget(targets);
|
const applicationTarget = findApplicationTarget(targets);
|
||||||
const buildNumberOverride =
|
const buildNumberOverride =
|
||||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||||
? await resolveRemoteBuildNumberAsync(ctx.graphqlClient, {
|
? await resolveRemoteBuildNumberAsync({
|
||||||
projectDir: ctx.projectDir,
|
projectDir: ctx.projectDir,
|
||||||
projectId: ctx.projectId,
|
projectId: ctx.projectId,
|
||||||
exp: ctx.exp,
|
exp: ctx.exp,
|
||||||
applicationTarget,
|
applicationTarget,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
vcsClient: ctx.vcsClient,
|
|
||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
return {
|
return {
|
||||||
@@ -88,14 +80,13 @@ export async function prepareIosBuildAsync(
|
|||||||
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
|
||||||
? false
|
? false
|
||||||
: ctx.buildProfile.autoIncrement,
|
: ctx.buildProfile.autoIncrement,
|
||||||
vcsClient: ctx.vcsClient,
|
nonInteractive: ctx.nonInteractive,
|
||||||
env: ctx.buildProfile.env,
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
prepareJobAsync: async (
|
prepareJobAsync: async (
|
||||||
ctx: BuildContext<Platform.IOS>,
|
ctx: BuildContext<Platform.IOS>,
|
||||||
jobData: JobData<IosCredentials>
|
jobData: JobData<IosCredentials>
|
||||||
): Promise<Ios.Job> => {
|
): Promise<Job> => {
|
||||||
return await prepareJobAsync(ctx, {
|
return await prepareJobAsync(ctx, {
|
||||||
...jobData,
|
...jobData,
|
||||||
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
|
||||||
@@ -109,7 +100,7 @@ export async function prepareIosBuildAsync(
|
|||||||
): Promise<BuildResult> => {
|
): Promise<BuildResult> => {
|
||||||
const graphqlMetadata = transformMetadata(metadata);
|
const graphqlMetadata = transformMetadata(metadata);
|
||||||
const graphqlJob = transformJob(job);
|
const graphqlJob = transformJob(job);
|
||||||
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
|
return await BuildMutation.createIosBuildAsync({
|
||||||
appId,
|
appId,
|
||||||
job: graphqlJob,
|
job: graphqlJob,
|
||||||
metadata: graphqlMetadata,
|
metadata: graphqlMetadata,
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
import { Platform } from '@expo/eas-build-job';
|
||||||
import { BuildProfile } from '@expo/eas-json';
|
|
||||||
|
|
||||||
import { CredentialsContext } from '../../credentials/context';
|
|
||||||
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
import IosCredentialsProvider from '../../credentials/ios/IosCredentialsProvider';
|
||||||
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
import { getAppFromContextAsync } from '../../credentials/ios/actions/BuildCredentialsUtils';
|
||||||
import { IosCredentials, Target } from '../../credentials/ios/types';
|
import { IosCredentials, Target } from '../../credentials/ios/types';
|
||||||
@@ -33,27 +31,6 @@ export async function ensureIosCredentialsAsync(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureIosCredentialsForBuildResignAsync(
|
|
||||||
credentialsCtx: CredentialsContext,
|
|
||||||
targets: Target[],
|
|
||||||
buildProfile: BuildProfile<Platform.IOS>
|
|
||||||
): Promise<CredentialsResult<IosCredentials>> {
|
|
||||||
const provider = new IosCredentialsProvider(credentialsCtx, {
|
|
||||||
app: await getAppFromContextAsync(credentialsCtx),
|
|
||||||
targets,
|
|
||||||
distribution: 'internal',
|
|
||||||
enterpriseProvisioning: buildProfile.enterpriseProvisioning,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { credentialsSource } = buildProfile;
|
|
||||||
|
|
||||||
logCredentialsSource(credentialsSource, Platform.IOS);
|
|
||||||
return {
|
|
||||||
credentials: await provider.getCredentialsAsync(credentialsSource),
|
|
||||||
source: credentialsSource,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
|
||||||
return !buildCtx.buildProfile.simulator && !buildCtx.buildProfile.withoutCredentials;
|
return !buildCtx.buildProfile.simulator;
|
||||||
}
|
}
|
||||||
@@ -2,22 +2,16 @@ import { Ios } from '@expo/eas-build-job';
|
|||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
|
|
||||||
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
|
||||||
import {
|
import { transformProjectArchive, transformWorkflow } from '../graphql';
|
||||||
loggerLevelToGraphQLWorkerLoggerLevel,
|
|
||||||
transformBuildMode,
|
|
||||||
transformBuildTrigger,
|
|
||||||
transformProjectArchive,
|
|
||||||
transformWorkflow,
|
|
||||||
} from '../graphql';
|
|
||||||
|
|
||||||
export function transformJob(job: Ios.Job): IosJobInput {
|
export function transformJob(job: Ios.Job): IosJobInput {
|
||||||
return {
|
return {
|
||||||
type: transformWorkflow(job.type),
|
type: transformWorkflow(job.type),
|
||||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
|
||||||
projectArchive: transformProjectArchive(job.projectArchive),
|
projectArchive: transformProjectArchive(job.projectArchive),
|
||||||
projectRootDirectory: nullthrows(job.projectRootDirectory),
|
projectRootDirectory: job.projectRootDirectory,
|
||||||
|
releaseChannel: job.releaseChannel,
|
||||||
updates: job.updates,
|
updates: job.updates,
|
||||||
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
|
secrets: transformIosSecrets(job.secrets),
|
||||||
builderEnvironment: job.builderEnvironment,
|
builderEnvironment: job.builderEnvironment,
|
||||||
cache: job.cache,
|
cache: job.cache,
|
||||||
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
|
version: job.version?.buildNumber ? { buildNumber: job.version.buildNumber } : undefined,
|
||||||
@@ -29,15 +23,10 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
|||||||
developmentClient: job.developmentClient,
|
developmentClient: job.developmentClient,
|
||||||
simulator: job.simulator,
|
simulator: job.simulator,
|
||||||
experimental: job.experimental,
|
experimental: job.experimental,
|
||||||
mode: transformBuildMode(job.mode),
|
|
||||||
customBuildConfig: job.customBuildConfig,
|
|
||||||
loggerLevel: job.loggerLevel
|
|
||||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformIosSecrets(secrets: {
|
function transformIosSecrets(secrets: {
|
||||||
buildCredentials?: Ios.BuildCredentials;
|
buildCredentials?: Ios.BuildCredentials;
|
||||||
}): IosJobSecretsInput {
|
}): IosJobSecretsInput {
|
||||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import {
|
import { ArchiveSource, Ios, Job, Platform, sanitizeJob } from '@expo/eas-build-job';
|
||||||
ArchiveSource,
|
|
||||||
BuildMode,
|
|
||||||
BuildTrigger,
|
|
||||||
Ios,
|
|
||||||
Platform,
|
|
||||||
sanitizeBuildJob,
|
|
||||||
} from '@expo/eas-build-job';
|
|
||||||
import { BuildProfile } from '@expo/eas-json';
|
|
||||||
import nullthrows from 'nullthrows';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import slash from 'slash';
|
import slash from 'slash';
|
||||||
|
|
||||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||||
import { IosJobSecretsInput } from '../../graphql/generated';
|
|
||||||
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
|
|
||||||
import { getUsername } from '../../project/projectUtils';
|
import { getUsername } from '../../project/projectUtils';
|
||||||
|
import { ensureLoggedInAsync } from '../../user/actions';
|
||||||
|
import { getVcsClient } from '../../vcs';
|
||||||
import { BuildContext } from '../context';
|
import { BuildContext } from '../context';
|
||||||
|
|
||||||
interface JobData {
|
interface JobData {
|
||||||
@@ -25,18 +16,21 @@ interface JobData {
|
|||||||
|
|
||||||
const cacheDefaults = {
|
const cacheDefaults = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
paths: [],
|
customPaths: [],
|
||||||
|
cacheDefaultPaths: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function prepareJobAsync(
|
export async function prepareJobAsync(
|
||||||
ctx: BuildContext<Platform.IOS>,
|
ctx: BuildContext<Platform.IOS>,
|
||||||
jobData: JobData
|
jobData: JobData
|
||||||
): Promise<Ios.Job> {
|
): Promise<Job> {
|
||||||
const username = getUsername(ctx.exp, ctx.user);
|
|
||||||
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
|
|
||||||
const projectRootDirectory =
|
const projectRootDirectory =
|
||||||
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
|
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
|
||||||
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
|
const username = getUsername(
|
||||||
|
ctx.exp,
|
||||||
|
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
|
||||||
|
);
|
||||||
|
const buildCredentials: Ios.Job['secrets']['buildCredentials'] = {};
|
||||||
if (jobData.credentials) {
|
if (jobData.credentials) {
|
||||||
const targetNames = Object.keys(jobData.credentials);
|
const targetNames = Object.keys(jobData.credentials);
|
||||||
for (const targetName of targetNames) {
|
for (const targetName of targetNames) {
|
||||||
@@ -44,50 +38,38 @@ export async function prepareJobAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const maybeCustomBuildConfigPath = buildProfile.config
|
|
||||||
? getCustomBuildConfigPathForJob(buildProfile.config)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
let buildMode;
|
|
||||||
if (ctx.repack) {
|
|
||||||
buildMode = BuildMode.REPACK;
|
|
||||||
} else if (buildProfile.config) {
|
|
||||||
buildMode = BuildMode.CUSTOM;
|
|
||||||
} else {
|
|
||||||
buildMode = BuildMode.BUILD;
|
|
||||||
}
|
|
||||||
|
|
||||||
const job: Ios.Job = {
|
const job: Ios.Job = {
|
||||||
type: ctx.workflow,
|
type: ctx.workflow,
|
||||||
platform: Platform.IOS,
|
platform: Platform.IOS,
|
||||||
projectArchive: jobData.projectArchive,
|
projectArchive: jobData.projectArchive,
|
||||||
projectRootDirectory,
|
projectRootDirectory,
|
||||||
builderEnvironment: {
|
builderEnvironment: {
|
||||||
image: buildProfile.image,
|
image: ctx.buildProfile.image,
|
||||||
node: buildProfile.node,
|
node: ctx.buildProfile.node,
|
||||||
pnpm: buildProfile.pnpm,
|
yarn: ctx.buildProfile.yarn,
|
||||||
bun: buildProfile.bun,
|
bundler: ctx.buildProfile.bundler,
|
||||||
yarn: buildProfile.yarn,
|
cocoapods: ctx.buildProfile.cocoapods,
|
||||||
bundler: buildProfile.bundler,
|
fastlane: ctx.buildProfile.fastlane,
|
||||||
cocoapods: buildProfile.cocoapods,
|
expoCli: ctx.buildProfile.expoCli,
|
||||||
fastlane: buildProfile.fastlane,
|
env: ctx.buildProfile.env,
|
||||||
env: buildProfile.env,
|
|
||||||
},
|
},
|
||||||
cache: {
|
cache: {
|
||||||
...cacheDefaults,
|
...cacheDefaults,
|
||||||
...buildProfile.cache,
|
...ctx.buildProfile.cache,
|
||||||
clear: ctx.clearCache,
|
clear: ctx.clearCache,
|
||||||
},
|
},
|
||||||
secrets: {
|
secrets: {
|
||||||
buildCredentials,
|
buildCredentials,
|
||||||
},
|
},
|
||||||
updates: { channel: buildProfile.channel },
|
releaseChannel: ctx.buildProfile.releaseChannel,
|
||||||
developmentClient: buildProfile.developmentClient,
|
updates: { channel: ctx.buildProfile.channel },
|
||||||
simulator: buildProfile.simulator,
|
developmentClient: ctx.buildProfile.developmentClient,
|
||||||
|
simulator: ctx.buildProfile.simulator,
|
||||||
scheme: jobData.buildScheme,
|
scheme: jobData.buildScheme,
|
||||||
buildConfiguration: buildProfile.buildConfiguration,
|
buildConfiguration: ctx.buildProfile.buildConfiguration,
|
||||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
applicationArchivePath:
|
||||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
ctx.buildProfile.applicationArchivePath ?? ctx.buildProfile.artifactPath,
|
||||||
|
buildArtifactPaths: ctx.buildProfile.buildArtifactPaths,
|
||||||
username,
|
username,
|
||||||
...(ctx.ios.buildNumberOverride && {
|
...(ctx.ios.buildNumberOverride && {
|
||||||
version: {
|
version: {
|
||||||
@@ -95,39 +77,10 @@ export async function prepareJobAsync(
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
experimental: {
|
experimental: {
|
||||||
prebuildCommand: buildProfile.prebuildCommand,
|
prebuildCommand: ctx.buildProfile.prebuildCommand,
|
||||||
},
|
},
|
||||||
mode: buildMode,
|
|
||||||
triggeredBy: BuildTrigger.EAS_CLI,
|
|
||||||
...(maybeCustomBuildConfigPath && {
|
|
||||||
customBuildConfig: {
|
|
||||||
path: maybeCustomBuildConfigPath,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(ctx.repack && {
|
|
||||||
customBuildConfig: {
|
|
||||||
path: '__eas/repack.yml',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
loggerLevel: ctx.loggerLevel,
|
|
||||||
};
|
};
|
||||||
return sanitizeBuildJob(job) as Ios.Job;
|
return sanitizeJob(job);
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
|
|
||||||
const buildCredentials: IosJobSecretsInput['buildCredentials'] = [];
|
|
||||||
for (const targetName of Object.keys(credentials ?? {})) {
|
|
||||||
buildCredentials.push({
|
|
||||||
targetName,
|
|
||||||
provisioningProfileBase64: nullthrows(credentials?.[targetName].provisioningProfile),
|
|
||||||
distributionCertificate: {
|
|
||||||
dataBase64: nullthrows(credentials?.[targetName].distributionCertificate.certificateP12),
|
|
||||||
password: nullthrows(credentials?.[targetName].distributionCertificate.certificatePassword),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { buildCredentials };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
function prepareTargetCredentials(targetCredentials: TargetCredentials): Ios.TargetCredentials {
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
import { ExpoConfig } from '@expo/config';
|
import { ExpoConfig } from '@expo/config';
|
||||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { IosVersionAutoIncrement } from '@expo/eas-json';
|
import { IosVersionAutoIncrement } from '@expo/eas-json';
|
||||||
|
|
||||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
|
||||||
import { Target } from '../../credentials/ios/types';
|
import { Target } from '../../credentials/ios/types';
|
||||||
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
|
||||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||||
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
|
||||||
import { Client } from '../../vcs/vcs';
|
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||||
|
|
||||||
export async function syncProjectConfigurationAsync({
|
export async function syncProjectConfigurationAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
targets,
|
targets,
|
||||||
localAutoIncrement,
|
localAutoIncrement,
|
||||||
vcsClient,
|
nonInteractive,
|
||||||
env,
|
|
||||||
}: {
|
}: {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
exp: ExpoConfig;
|
exp: ExpoConfig;
|
||||||
targets: Target[];
|
targets: Target[];
|
||||||
localAutoIncrement?: IosVersionAutoIncrement;
|
localAutoIncrement?: IosVersionAutoIncrement;
|
||||||
vcsClient: Client;
|
nonInteractive: boolean;
|
||||||
env: Env | undefined;
|
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
|
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||||
|
|
||||||
if (workflow === Workflow.GENERIC) {
|
if (workflow === Workflow.GENERIC) {
|
||||||
if (isExpoUpdatesInstalled(projectDir)) {
|
if (isExpoUpdatesInstalled(projectDir)) {
|
||||||
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env });
|
await syncUpdatesConfigurationAsync(projectDir, exp, { nonInteractive });
|
||||||
}
|
}
|
||||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
|
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import chalk from 'chalk';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import type { XCBuildConfiguration } from 'xcode';
|
import type { XCBuildConfiguration } from 'xcode';
|
||||||
|
|
||||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { Target } from '../../credentials/ios/types';
|
import { Target } from '../../credentials/ios/types';
|
||||||
import { AppPlatform } from '../../graphql/generated';
|
import { AppPlatform } from '../../graphql/generated';
|
||||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||||
@@ -19,12 +18,9 @@ import { resolveWorkflowAsync } from '../../project/workflow';
|
|||||||
import { promptAsync } from '../../prompts';
|
import { promptAsync } from '../../prompts';
|
||||||
import uniqBy from '../../utils/expodash/uniqBy';
|
import uniqBy from '../../utils/expodash/uniqBy';
|
||||||
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
|
||||||
import { Client } from '../../vcs/vcs';
|
|
||||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||||
|
|
||||||
const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
|
|
||||||
|
|
||||||
export enum BumpStrategy {
|
export enum BumpStrategy {
|
||||||
APP_VERSION,
|
APP_VERSION,
|
||||||
BUILD_NUMBER,
|
BUILD_NUMBER,
|
||||||
@@ -100,32 +96,19 @@ export async function bumpVersionInAppJsonAsync({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateShortVersion(shortVersion: string | undefined): void {
|
|
||||||
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
|
|
||||||
throw new Error(
|
|
||||||
`CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: ${shortVersion}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readShortVersionAsync(
|
export async function readShortVersionAsync(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
exp: ExpoConfig,
|
exp: ExpoConfig,
|
||||||
buildSettings: XCBuildConfiguration['buildSettings'],
|
buildSettings: XCBuildConfiguration['buildSettings']
|
||||||
vcsClient: Client
|
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
|
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||||
if (workflow === Workflow.GENERIC) {
|
if (workflow === Workflow.GENERIC) {
|
||||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||||
|
return (
|
||||||
const shortVersion =
|
|
||||||
infoPlist.CFBundleShortVersionString &&
|
infoPlist.CFBundleShortVersionString &&
|
||||||
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
|
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings)
|
||||||
|
);
|
||||||
validateShortVersion(shortVersion);
|
|
||||||
return shortVersion;
|
|
||||||
} else {
|
} else {
|
||||||
validateShortVersion(exp.version);
|
|
||||||
return exp.version;
|
return exp.version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,10 +116,9 @@ export async function readShortVersionAsync(
|
|||||||
export async function readBuildNumberAsync(
|
export async function readBuildNumberAsync(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
exp: ExpoConfig,
|
exp: ExpoConfig,
|
||||||
buildSettings: XCBuildConfiguration['buildSettings'],
|
buildSettings: XCBuildConfiguration['buildSettings']
|
||||||
vcsClient: Client
|
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
|
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
|
||||||
if (workflow === Workflow.GENERIC) {
|
if (workflow === Workflow.GENERIC) {
|
||||||
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
|
||||||
return (
|
return (
|
||||||
@@ -150,8 +132,7 @@ export async function readBuildNumberAsync(
|
|||||||
export async function maybeResolveVersionsAsync(
|
export async function maybeResolveVersionsAsync(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
exp: ExpoConfig,
|
exp: ExpoConfig,
|
||||||
targets: Target[],
|
targets: Target[]
|
||||||
vcsClient: Client
|
|
||||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||||
const applicationTarget = findApplicationTarget(targets);
|
const applicationTarget = findApplicationTarget(targets);
|
||||||
try {
|
try {
|
||||||
@@ -159,14 +140,12 @@ export async function maybeResolveVersionsAsync(
|
|||||||
appBuildVersion: await readBuildNumberAsync(
|
appBuildVersion: await readBuildNumberAsync(
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
applicationTarget.buildSettings ?? {},
|
applicationTarget.buildSettings ?? {}
|
||||||
vcsClient
|
|
||||||
),
|
),
|
||||||
appVersion: await readShortVersionAsync(
|
appVersion: await readShortVersionAsync(
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
applicationTarget.buildSettings ?? {},
|
applicationTarget.buildSettings ?? {}
|
||||||
vcsClient
|
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -276,26 +255,20 @@ export function evaluateTemplateString(
|
|||||||
* Returns buildNumber that will be used for the next build. If current build profile
|
* Returns buildNumber that will be used for the next build. If current build profile
|
||||||
* has an 'autoIncrement' option set, it increments the version on server.
|
* has an 'autoIncrement' option set, it increments the version on server.
|
||||||
*/
|
*/
|
||||||
export async function resolveRemoteBuildNumberAsync(
|
export async function resolveRemoteBuildNumberAsync({
|
||||||
graphqlClient: ExpoGraphqlClient,
|
projectDir,
|
||||||
{
|
projectId,
|
||||||
projectDir,
|
exp,
|
||||||
projectId,
|
applicationTarget,
|
||||||
exp,
|
buildProfile,
|
||||||
applicationTarget,
|
}: {
|
||||||
buildProfile,
|
projectDir: string;
|
||||||
vcsClient,
|
projectId: string;
|
||||||
}: {
|
exp: ExpoConfig;
|
||||||
projectDir: string;
|
applicationTarget: Target;
|
||||||
projectId: string;
|
buildProfile: BuildProfile<Platform.IOS>;
|
||||||
exp: ExpoConfig;
|
}): Promise<string> {
|
||||||
applicationTarget: Target;
|
|
||||||
buildProfile: BuildProfile<Platform.IOS>;
|
|
||||||
vcsClient: Client;
|
|
||||||
}
|
|
||||||
): Promise<string> {
|
|
||||||
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
const remoteVersions = await AppVersionQuery.latestVersionAsync(
|
||||||
graphqlClient,
|
|
||||||
projectId,
|
projectId,
|
||||||
AppPlatform.Ios,
|
AppPlatform.Ios,
|
||||||
applicationTarget.bundleIdentifier
|
applicationTarget.bundleIdentifier
|
||||||
@@ -304,14 +277,12 @@ export async function resolveRemoteBuildNumberAsync(
|
|||||||
const localBuildNumber = await readBuildNumberAsync(
|
const localBuildNumber = await readBuildNumberAsync(
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
applicationTarget.buildSettings ?? {},
|
applicationTarget.buildSettings ?? {}
|
||||||
vcsClient
|
|
||||||
);
|
);
|
||||||
const localShortVersion = await readShortVersionAsync(
|
const localShortVersion = await readShortVersionAsync(
|
||||||
projectDir,
|
projectDir,
|
||||||
exp,
|
exp,
|
||||||
applicationTarget.buildSettings ?? {},
|
applicationTarget.buildSettings ?? {}
|
||||||
vcsClient
|
|
||||||
);
|
);
|
||||||
let currentBuildVersion: string;
|
let currentBuildVersion: string;
|
||||||
if (remoteVersions?.buildVersion) {
|
if (remoteVersions?.buildVersion) {
|
||||||
@@ -336,15 +307,13 @@ export async function resolveRemoteBuildNumberAsync(
|
|||||||
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
|
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
|
||||||
).start();
|
).start();
|
||||||
try {
|
try {
|
||||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
await AppVersionMutation.createAppVersionAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
platform: AppPlatform.Ios,
|
platform: AppPlatform.Ios,
|
||||||
applicationIdentifier: applicationTarget.bundleIdentifier,
|
applicationIdentifier: applicationTarget.bundleIdentifier,
|
||||||
storeVersion: localShortVersion ?? '1.0.0',
|
storeVersion: localShortVersion ?? '1.0.0',
|
||||||
buildVersion: currentBuildVersion,
|
buildVersion: currentBuildVersion,
|
||||||
runtimeVersion:
|
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
|
||||||
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
|
|
||||||
undefined,
|
|
||||||
});
|
});
|
||||||
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
|
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -360,15 +329,13 @@ export async function resolveRemoteBuildNumberAsync(
|
|||||||
)}.`
|
)}.`
|
||||||
).start();
|
).start();
|
||||||
try {
|
try {
|
||||||
await AppVersionMutation.createAppVersionAsync(graphqlClient, {
|
await AppVersionMutation.createAppVersionAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
platform: AppPlatform.Ios,
|
platform: AppPlatform.Ios,
|
||||||
applicationIdentifier: applicationTarget.bundleIdentifier,
|
applicationIdentifier: applicationTarget.bundleIdentifier,
|
||||||
storeVersion: localShortVersion ?? '1.0.0',
|
storeVersion: localShortVersion ?? '1.0.0',
|
||||||
buildVersion: nextBuildVersion,
|
buildVersion: nextBuildVersion,
|
||||||
runtimeVersion:
|
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
|
||||||
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
|
|
||||||
undefined,
|
|
||||||
});
|
});
|
||||||
spinner.succeed(
|
spinner.succeed(
|
||||||
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
|
||||||
|
|||||||
@@ -6,28 +6,10 @@ import semver from 'semver';
|
|||||||
import { ora } from '../ora';
|
import { ora } from '../ora';
|
||||||
|
|
||||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||||
const PLUGIN_PACKAGE_VERSION = '1.0.118';
|
const PLUGIN_PACKAGE_VERSION = '0.0.99';
|
||||||
|
|
||||||
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 {
|
export interface LocalBuildOptions {
|
||||||
localBuildMode?: LocalBuildMode;
|
enable: boolean;
|
||||||
skipCleanup?: boolean;
|
skipCleanup?: boolean;
|
||||||
skipNativeBuild?: boolean;
|
skipNativeBuild?: boolean;
|
||||||
artifactsDir?: string;
|
artifactsDir?: string;
|
||||||
|
|||||||
@@ -1,56 +1,58 @@
|
|||||||
|
import { Updates } from '@expo/config-plugins';
|
||||||
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
|
||||||
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
import { IosEnterpriseProvisioning } from '@expo/eas-json';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import resolveFrom from 'resolve-from';
|
import resolveFrom from 'resolve-from';
|
||||||
|
|
||||||
|
import Log from '../log';
|
||||||
|
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
|
||||||
|
import {
|
||||||
|
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||||
|
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||||
|
} from '../update/android/UpdatesModule';
|
||||||
|
import {
|
||||||
|
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||||
|
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||||
|
} from '../update/ios/UpdatesModule';
|
||||||
|
import { ensureLoggedInAsync } from '../user/actions';
|
||||||
|
import { easCliVersion } from '../utils/easCli';
|
||||||
|
import { getVcsClient } from '../vcs';
|
||||||
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
|
||||||
import { BuildContext } from './context';
|
import { BuildContext } from './context';
|
||||||
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
|
||||||
import { LocalBuildMode } from './local';
|
|
||||||
import { BuildDistributionType } from './types';
|
|
||||||
import Log from '../log';
|
|
||||||
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
|
|
||||||
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
|
|
||||||
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
|
|
||||||
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
|
|
||||||
import { easCliVersion } from '../utils/easCli';
|
|
||||||
|
|
||||||
export async function collectMetadataAsync<T extends Platform>(
|
export async function collectMetadataAsync<T extends Platform>(
|
||||||
ctx: BuildContext<T>
|
ctx: BuildContext<T>
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
const channelObject = await resolveChannelAsync(ctx);
|
const vcsClient = getVcsClient();
|
||||||
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE;
|
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||||
|
const distribution =
|
||||||
|
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
|
||||||
|
? 'simulator'
|
||||||
|
: ctx.buildProfile.distribution) ?? 'store';
|
||||||
const metadata: Metadata = {
|
const metadata: Metadata = {
|
||||||
trackingContext: ctx.analyticsEventProperties,
|
trackingContext: ctx.trackingCtx,
|
||||||
...(await maybeResolveVersionsAsync(ctx)),
|
...(await maybeResolveVersionsAsync(ctx)),
|
||||||
cliVersion: easCliVersion,
|
cliVersion: easCliVersion,
|
||||||
workflow: ctx.workflow,
|
workflow: ctx.workflow,
|
||||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||||
sdkVersion: ctx.exp.sdkVersion,
|
sdkVersion: ctx.exp.sdkVersion,
|
||||||
runtimeVersion:
|
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
|
||||||
(await resolveRuntimeVersionAsync({
|
reactNativeVersion: await getReactNativeVersionAsync(ctx),
|
||||||
exp: ctx.exp,
|
...channelOrReleaseChannel,
|
||||||
platform: ctx.platform,
|
|
||||||
workflow: ctx.workflow,
|
|
||||||
projectDir: ctx.projectDir,
|
|
||||||
env: ctx.buildProfile.env,
|
|
||||||
cwd: ctx.projectDir,
|
|
||||||
})) ?? undefined,
|
|
||||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
|
||||||
...channelObject,
|
|
||||||
distribution,
|
distribution,
|
||||||
appName: ctx.exp.name,
|
appName: ctx.exp.name,
|
||||||
appIdentifier: resolveAppIdentifier(ctx),
|
appIdentifier: resolveAppIdentifier(ctx),
|
||||||
buildProfile: ctx.buildProfileName,
|
buildProfile: ctx.buildProfileName,
|
||||||
gitCommitHash: await ctx.vcsClient.getCommitHashAsync(),
|
gitCommitHash: await vcsClient.getCommitHashAsync(),
|
||||||
gitCommitMessage: truncateGitCommitMessage(
|
gitCommitMessage: truncateGitCommitMessage(
|
||||||
(await ctx.vcsClient.getLastCommitMessageAsync()) ?? undefined
|
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
|
||||||
|
),
|
||||||
|
isGitWorkingTreeDirty: await vcsClient.hasUncommittedChangesAsync(),
|
||||||
|
username: getUsername(
|
||||||
|
ctx.exp,
|
||||||
|
await ensureLoggedInAsync({ nonInteractive: ctx.nonInteractive })
|
||||||
),
|
),
|
||||||
isGitWorkingTreeDirty:
|
|
||||||
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
|
|
||||||
? false
|
|
||||||
: await ctx.vcsClient.hasUncommittedChangesAsync(),
|
|
||||||
username: getUsername(ctx.exp, ctx.user),
|
|
||||||
message: ctx.message,
|
message: ctx.message,
|
||||||
...(ctx.platform === Platform.IOS && {
|
...(ctx.platform === Platform.IOS && {
|
||||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||||
@@ -59,12 +61,6 @@ export async function collectMetadataAsync<T extends Platform>(
|
|||||||
}),
|
}),
|
||||||
runWithNoWaitFlag: ctx.noWait,
|
runWithNoWaitFlag: ctx.noWait,
|
||||||
runFromCI: ctx.runFromCI,
|
runFromCI: ctx.runFromCI,
|
||||||
customWorkflowName: ctx.customBuildConfigMetadata?.workflowName,
|
|
||||||
developmentClient: ctx.developmentClient,
|
|
||||||
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
|
|
||||||
selectedImage: ctx.buildProfile.image,
|
|
||||||
customNodeVersion: ctx.buildProfile.node,
|
|
||||||
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
|
|
||||||
};
|
};
|
||||||
return sanitizeMetadata(metadata);
|
return sanitizeMetadata(metadata);
|
||||||
}
|
}
|
||||||
@@ -77,8 +73,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
|
|||||||
const resolvedVersion = await maybeResolveIosVersionsAsync(
|
const resolvedVersion = await maybeResolveIosVersionsAsync(
|
||||||
ctx.projectDir,
|
ctx.projectDir,
|
||||||
ctx.exp,
|
ctx.exp,
|
||||||
iosContext.ios.targets,
|
iosContext.ios.targets
|
||||||
ctx.vcsClient
|
|
||||||
);
|
);
|
||||||
if (iosContext.ios.buildNumberOverride) {
|
if (iosContext.ios.buildNumberOverride) {
|
||||||
return {
|
return {
|
||||||
@@ -92,8 +87,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
|
|||||||
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
|
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
|
||||||
ctx.projectDir,
|
ctx.projectDir,
|
||||||
ctx.exp,
|
ctx.exp,
|
||||||
androidCtx.buildProfile,
|
androidCtx.buildProfile
|
||||||
ctx.vcsClient
|
|
||||||
);
|
);
|
||||||
if (androidCtx.android.versionCodeOverride) {
|
if (androidCtx.android.versionCodeOverride) {
|
||||||
return {
|
return {
|
||||||
@@ -115,21 +109,39 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveChannelAsync<T extends Platform>(
|
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||||
ctx: BuildContext<T>
|
ctx: BuildContext<T>
|
||||||
): Promise<{ channel: string } | null> {
|
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (ctx.buildProfile.channel) {
|
if (ctx.buildProfile.channel) {
|
||||||
return { channel: ctx.buildProfile.channel };
|
return { channel: ctx.buildProfile.channel };
|
||||||
}
|
}
|
||||||
|
if (ctx.buildProfile.releaseChannel) {
|
||||||
|
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||||
|
}
|
||||||
const channel = await getNativeChannelAsync(ctx);
|
const channel = await getNativeChannelAsync(ctx);
|
||||||
if (channel) {
|
if (channel) {
|
||||||
return { channel };
|
return { channel };
|
||||||
}
|
}
|
||||||
|
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||||
|
return { releaseChannel };
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
async function getNativeReleaseChannelAsync<T extends Platform>(
|
||||||
|
ctx: BuildContext<T>
|
||||||
|
): Promise<string> {
|
||||||
|
switch (ctx.platform) {
|
||||||
|
case Platform.ANDROID: {
|
||||||
|
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||||
|
}
|
||||||
|
case Platform.IOS: {
|
||||||
|
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNativeChannelAsync<T extends Platform>(
|
async function getNativeChannelAsync<T extends Platform>(
|
||||||
@@ -147,9 +159,11 @@ async function getNativeChannelAsync<T extends Platform>(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getReactNativeVersionAsync(projectDir: string): Promise<string | undefined> {
|
async function getReactNativeVersionAsync(
|
||||||
|
ctx: BuildContext<Platform>
|
||||||
|
): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const reactNativePackageJsonPath = resolveFrom(projectDir, 'react-native/package.json');
|
const reactNativePackageJsonPath = resolveFrom(ctx.projectDir, 'react-native/package.json');
|
||||||
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
return (await fs.readJson(reactNativePackageJsonPath)).version;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Log.debug('Failed to resolve react-native version:');
|
Log.debug('Failed to resolve react-native version:');
|
||||||
|
|||||||
@@ -1,36 +1,28 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
import { formatGraphQLBuild } from './utils/formatBuild';
|
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||||
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
|
import { BuildFilter, BuildFragment } from '../graphql/generated';
|
||||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||||
import Log from '../log';
|
import Log from '../log';
|
||||||
import { fromNow } from '../utils/date';
|
|
||||||
import { printJsonOnlyOutput } from '../utils/json';
|
import { printJsonOnlyOutput } from '../utils/json';
|
||||||
import {
|
import { paginatedQueryWithConfirmPromptAsync } from '../utils/queries';
|
||||||
paginatedQueryWithConfirmPromptAsync,
|
import { formatGraphQLBuild } from './utils/formatBuild';
|
||||||
paginatedQueryWithSelectPromptAsync,
|
|
||||||
} from '../utils/queries';
|
|
||||||
|
|
||||||
export const BUILDS_LIMIT = 50;
|
export const BUILDS_LIMIT = 50;
|
||||||
|
|
||||||
export async function listAndRenderBuildsOnAppAsync(
|
export async function listAndRenderBuildsOnAppAsync({
|
||||||
graphqlClient: ExpoGraphqlClient,
|
projectId,
|
||||||
{
|
projectDisplayName,
|
||||||
projectId,
|
filter,
|
||||||
projectDisplayName,
|
paginatedQueryOptions,
|
||||||
filter,
|
}: {
|
||||||
paginatedQueryOptions,
|
projectId: string;
|
||||||
}: {
|
projectDisplayName: string;
|
||||||
projectId: string;
|
filter?: BuildFilter;
|
||||||
projectDisplayName: string;
|
paginatedQueryOptions: PaginatedQueryOptions;
|
||||||
filter?: BuildFilter;
|
}): Promise<void> {
|
||||||
paginatedQueryOptions: PaginatedQueryOptions;
|
|
||||||
}
|
|
||||||
): Promise<void> {
|
|
||||||
if (paginatedQueryOptions.nonInteractive) {
|
if (paginatedQueryOptions.nonInteractive) {
|
||||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
const builds = await BuildQuery.viewBuildsOnAppAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||||
offset: paginatedQueryOptions.offset,
|
offset: paginatedQueryOptions.offset,
|
||||||
@@ -42,7 +34,7 @@ export async function listAndRenderBuildsOnAppAsync(
|
|||||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
||||||
offset: paginatedQueryOptions.offset,
|
offset: paginatedQueryOptions.offset,
|
||||||
queryToPerform: (limit, offset) =>
|
queryToPerform: (limit, offset) =>
|
||||||
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
BuildQuery.viewBuildsOnAppAsync({
|
||||||
appId: projectId,
|
appId: projectId,
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
@@ -57,122 +49,6 @@ export async function listAndRenderBuildsOnAppAsync(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listAndSelectBuildOnAppAsync(
|
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
title,
|
|
||||||
filter,
|
|
||||||
paginatedQueryOptions,
|
|
||||||
selectPromptDisabledFunction,
|
|
||||||
selectPromptWarningMessage,
|
|
||||||
}: {
|
|
||||||
projectId: string;
|
|
||||||
title: string;
|
|
||||||
filter?: BuildFilter;
|
|
||||||
paginatedQueryOptions: PaginatedQueryOptions;
|
|
||||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean;
|
|
||||||
selectPromptWarningMessage?: string;
|
|
||||||
}
|
|
||||||
): Promise<BuildFragment | null> {
|
|
||||||
if (paginatedQueryOptions.nonInteractive) {
|
|
||||||
throw new Error('Unable to select a build in non-interactive mode.');
|
|
||||||
} else {
|
|
||||||
const selectedBuild = await paginatedQueryWithSelectPromptAsync({
|
|
||||||
limit: paginatedQueryOptions.limit ?? BUILDS_LIMIT,
|
|
||||||
offset: paginatedQueryOptions.offset,
|
|
||||||
queryToPerform: (limit, offset) =>
|
|
||||||
BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
|
||||||
appId: projectId,
|
|
||||||
limit,
|
|
||||||
offset,
|
|
||||||
filter,
|
|
||||||
}),
|
|
||||||
promptOptions: {
|
|
||||||
title,
|
|
||||||
getIdentifierForQueryItem: build => build.id,
|
|
||||||
makePartialChoiceObject: createBuildToPartialChoiceMaker(selectPromptDisabledFunction),
|
|
||||||
selectPromptWarningMessage,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return selectedBuild ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBuildToPartialChoiceMaker(
|
|
||||||
selectPromptDisabledFunction?: (build: BuildFragment) => boolean
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
build: BuildFragment
|
|
||||||
): {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
} => {
|
|
||||||
const splitCommitMessage = build.gitCommitMessage?.split('\n');
|
|
||||||
const formattedCommitData =
|
|
||||||
build.gitCommitHash && splitCommitMessage && splitCommitMessage.length > 0
|
|
||||||
? `${build.gitCommitHash.slice(0, 7)} "${chalk.bold(
|
|
||||||
splitCommitMessage[0] + (splitCommitMessage.length > 1 ? '…' : '')
|
|
||||||
)}"`
|
|
||||||
: 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,
|
|
||||||
},
|
|
||||||
{ name: 'Profile', value: build.buildProfile ? chalk.bold(build.buildProfile) : 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: filteredDescriptionArray.length > 0 ? filteredDescriptionArray.join('\n') : '',
|
|
||||||
disabled: selectPromptDisabledFunction?.(build),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getLatestBuildAsync(
|
|
||||||
graphqlClient: ExpoGraphqlClient,
|
|
||||||
{
|
|
||||||
projectId,
|
|
||||||
filter,
|
|
||||||
}: {
|
|
||||||
projectId: string;
|
|
||||||
filter?: BuildFilter;
|
|
||||||
}
|
|
||||||
): Promise<BuildFragment | null> {
|
|
||||||
const builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
|
||||||
appId: projectId,
|
|
||||||
limit: 1,
|
|
||||||
offset: 0,
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (builds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return builds[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPageOfBuilds({
|
function renderPageOfBuilds({
|
||||||
builds,
|
builds,
|
||||||
projectDisplayName,
|
projectDisplayName,
|
||||||
|
|||||||
@@ -1,68 +1,39 @@
|
|||||||
import { ExpoConfig } from '@expo/config-types';
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
|
||||||
import {
|
import {
|
||||||
AppVersionSource,
|
AppVersionSource,
|
||||||
BuildProfile,
|
BuildProfile,
|
||||||
EasJson,
|
EasJson,
|
||||||
EasJsonAccessor,
|
EasJsonAccessor,
|
||||||
EasJsonUtils,
|
EasJsonUtils,
|
||||||
ResourceClass,
|
|
||||||
SubmitProfile,
|
SubmitProfile,
|
||||||
} from '@expo/eas-json';
|
} from '@expo/eas-json';
|
||||||
import { LoggerLevel } from '@expo/logger';
|
|
||||||
import assert from 'assert';
|
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
|
|
||||||
import { prepareAndroidBuildAsync } from './android/build';
|
|
||||||
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
|
|
||||||
import { ensureProjectConfiguredAsync } from './configure';
|
|
||||||
import { BuildContext } from './context';
|
|
||||||
import { createBuildContextAsync } from './createContext';
|
|
||||||
import { prepareIosBuildAsync } from './ios/build';
|
|
||||||
import { LocalBuildMode, LocalBuildOptions } from './local';
|
|
||||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
|
||||||
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
|
|
||||||
import { ensureRepoIsCleanAsync } from './utils/repository';
|
|
||||||
import { Analytics } from '../analytics/AnalyticsManager';
|
|
||||||
import { createAndLinkChannelAsync, doesChannelExistAsync } from '../channel/queries';
|
|
||||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
|
||||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
|
||||||
import {
|
import {
|
||||||
AppPlatform,
|
AppPlatform,
|
||||||
BuildFragment,
|
BuildFragment,
|
||||||
|
BuildResourceClass,
|
||||||
BuildStatus,
|
BuildStatus,
|
||||||
BuildWithSubmissionsFragment,
|
BuildWithSubmissionsFragment,
|
||||||
SubmissionFragment,
|
SubmissionFragment,
|
||||||
} from '../graphql/generated';
|
} from '../graphql/generated';
|
||||||
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||||
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
|
||||||
import Log, { learnMore } from '../log';
|
import Log from '../log';
|
||||||
import {
|
import {
|
||||||
RequestedPlatform,
|
RequestedPlatform,
|
||||||
appPlatformDisplayNames,
|
appPlatformDisplayNames,
|
||||||
appPlatformEmojis,
|
appPlatformEmojis,
|
||||||
toPlatforms,
|
toPlatforms,
|
||||||
} from '../platform';
|
} from '../platform';
|
||||||
import {
|
|
||||||
CustomBuildConfigMetadata,
|
|
||||||
validateCustomBuildConfigAsync,
|
|
||||||
} from '../project/customBuildConfig';
|
|
||||||
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
|
||||||
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
|
||||||
import {
|
import { validateAppVersionRuntimePolicySupportAsync } from '../project/projectUtils';
|
||||||
isExpoUpdatesInstalledAsDevDependency,
|
|
||||||
isExpoUpdatesInstalledOrAvailable,
|
|
||||||
isUsingEASUpdate,
|
|
||||||
validateAppVersionRuntimePolicySupportAsync,
|
|
||||||
} from '../project/projectUtils';
|
|
||||||
import {
|
import {
|
||||||
validateAppConfigForRemoteVersionSource,
|
validateAppConfigForRemoteVersionSource,
|
||||||
validateBuildProfileVersionSettings,
|
validateBuildProfileVersionSettings,
|
||||||
} from '../project/remoteVersionSource';
|
} from '../project/remoteVersionSource';
|
||||||
import { confirmAsync } from '../prompts';
|
|
||||||
import { runAsync } from '../run/run';
|
|
||||||
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
|
|
||||||
import { createSubmissionContextAsync } from '../submit/context';
|
import { createSubmissionContextAsync } from '../submit/context';
|
||||||
import {
|
import {
|
||||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
|
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish,
|
||||||
@@ -70,13 +41,20 @@ import {
|
|||||||
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
|
||||||
} from '../submit/submit';
|
} from '../submit/submit';
|
||||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||||
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
|
|
||||||
import { Actor } from '../user/User';
|
|
||||||
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
|
|
||||||
import { truthy } from '../utils/expodash/filter';
|
|
||||||
import { printJsonOnlyOutput } from '../utils/json';
|
import { printJsonOnlyOutput } from '../utils/json';
|
||||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||||
import { Client } from '../vcs/vcs';
|
import { getVcsClient } from '../vcs';
|
||||||
|
import { prepareAndroidBuildAsync } from './android/build';
|
||||||
|
import { BuildRequestSender, waitForBuildEndAsync } from './build';
|
||||||
|
import { ensureProjectConfiguredAsync } from './configure';
|
||||||
|
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';
|
||||||
|
|
||||||
let metroConfigValidated = false;
|
let metroConfigValidated = false;
|
||||||
let sdkVersionChecked = false;
|
let sdkVersionChecked = false;
|
||||||
@@ -91,33 +69,33 @@ export interface BuildFlags {
|
|||||||
autoSubmit: boolean;
|
autoSubmit: boolean;
|
||||||
submitProfile?: string;
|
submitProfile?: string;
|
||||||
localBuildOptions: LocalBuildOptions;
|
localBuildOptions: LocalBuildOptions;
|
||||||
resourceClass?: ResourceClass;
|
userInputResourceClass?: UserInputResourceClass;
|
||||||
message?: string;
|
message?: string;
|
||||||
buildLoggerLevel?: LoggerLevel;
|
|
||||||
freezeCredentials: boolean;
|
|
||||||
repack: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runBuildAndSubmitAsync(
|
const platformToGraphQLResourceClassMapping: Record<
|
||||||
graphqlClient: ExpoGraphqlClient,
|
Platform,
|
||||||
analytics: Analytics,
|
Record<UserInputResourceClass, BuildResourceClass>
|
||||||
vcsClient: Client,
|
> = {
|
||||||
projectDir: string,
|
[Platform.ANDROID]: {
|
||||||
flags: BuildFlags,
|
[UserInputResourceClass.DEFAULT]: BuildResourceClass.AndroidDefault,
|
||||||
actor: Actor,
|
[UserInputResourceClass.LARGE]: BuildResourceClass.AndroidLarge,
|
||||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
|
},
|
||||||
): Promise<{
|
[Platform.IOS]: {
|
||||||
buildIds: string[];
|
[UserInputResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
||||||
}> {
|
[UserInputResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
||||||
await vcsClient.ensureRepoExistsAsync();
|
},
|
||||||
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
|
};
|
||||||
|
|
||||||
|
export async function runBuildAndSubmitAsync(projectDir: string, flags: BuildFlags): Promise<void> {
|
||||||
|
await getVcsClient().ensureRepoExistsAsync();
|
||||||
|
await ensureRepoIsCleanAsync(flags.nonInteractive);
|
||||||
|
|
||||||
await ensureProjectConfiguredAsync({
|
await ensureProjectConfiguredAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
nonInteractive: flags.nonInteractive,
|
nonInteractive: flags.nonInteractive,
|
||||||
vcsClient,
|
|
||||||
});
|
});
|
||||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
const easJsonAccessor = new EasJsonAccessor(projectDir);
|
||||||
const easJsonCliConfig: EasJson['cli'] =
|
const easJsonCliConfig: EasJson['cli'] =
|
||||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||||
|
|
||||||
@@ -127,43 +105,16 @@ export async function runBuildAndSubmitAsync(
|
|||||||
easJsonAccessor,
|
easJsonAccessor,
|
||||||
platforms,
|
platforms,
|
||||||
profileName: flags.profile ?? undefined,
|
profileName: flags.profile ?? undefined,
|
||||||
projectDir,
|
|
||||||
});
|
});
|
||||||
Log.log(
|
|
||||||
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
|
|
||||||
buildProfiles[0].profile.env
|
|
||||||
? Object.keys(buildProfiles[0].profile.env).join(', ')
|
|
||||||
: 'no environment variables specified'
|
|
||||||
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const buildProfile of buildProfiles) {
|
|
||||||
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
|
|
||||||
Log.warn(
|
|
||||||
`The "image" field in the build profile "${buildProfile.profileName}" is set to "${buildProfile.profile.image}". This tag is deprecated and will be removed in the future. Use other images or tags listed here: https://docs.expo.dev/build-reference/infrastructure/`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
nonInteractive: flags.nonInteractive,
|
nonInteractive: flags.nonInteractive,
|
||||||
buildProfiles,
|
buildProfiles,
|
||||||
vcsClient,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
|
|
||||||
{};
|
|
||||||
for (const buildProfile of buildProfiles) {
|
for (const buildProfile of buildProfiles) {
|
||||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||||
const maybeMetadata = await validateCustomBuildConfigAsync({
|
|
||||||
projectDir,
|
|
||||||
profile: buildProfile.profile,
|
|
||||||
vcsClient,
|
|
||||||
});
|
|
||||||
if (maybeMetadata) {
|
|
||||||
customBuildConfigMetadataByPlatform[toAppPlatform(buildProfile.platform)] = maybeMetadata;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startedBuilds: {
|
const startedBuilds: {
|
||||||
@@ -173,45 +124,30 @@ export async function runBuildAndSubmitAsync(
|
|||||||
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
const buildCtxByPlatform: { [p in AppPlatform]?: BuildContext<Platform> } = {};
|
||||||
|
|
||||||
for (const buildProfile of buildProfiles) {
|
for (const buildProfile of buildProfiles) {
|
||||||
const platform = toAppPlatform(buildProfile.platform);
|
|
||||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
flags,
|
flags,
|
||||||
moreBuilds: platforms.length > 1,
|
moreBuilds: platforms.length > 1,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
|
resourceClass:
|
||||||
|
platformToGraphQLResourceClassMapping[buildProfile.platform][
|
||||||
|
flags.userInputResourceClass ?? UserInputResourceClass.DEFAULT
|
||||||
|
],
|
||||||
easJsonCliConfig,
|
easJsonCliConfig,
|
||||||
actor,
|
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
vcsClient,
|
|
||||||
getDynamicPrivateProjectConfigAsync,
|
|
||||||
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
|
|
||||||
});
|
});
|
||||||
if (maybeBuild) {
|
if (maybeBuild) {
|
||||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||||
}
|
}
|
||||||
buildCtxByPlatform[platform] = buildCtx;
|
buildCtxByPlatform[toAppPlatform(buildProfile.platform)] = buildCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
if (flags.localBuildOptions.enable) {
|
||||||
return {
|
return;
|
||||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
Log.newLine();
|
||||||
const startedBuild = await BuildQuery.byIdAsync(
|
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
||||||
graphqlClient,
|
Log.newLine();
|
||||||
nullthrows(process.env.EAS_BUILD_ID, 'EAS_BUILD_ID is not defined')
|
|
||||||
);
|
|
||||||
startedBuilds.push({ build: startedBuild, buildProfile: buildProfiles[0] });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!flags.localBuildOptions.localBuildMode) {
|
|
||||||
Log.newLine();
|
|
||||||
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
|
|
||||||
Log.newLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
const submissions: SubmissionFragment[] = [];
|
const submissions: SubmissionFragment[] = [];
|
||||||
if (flags.autoSubmit) {
|
if (flags.autoSubmit) {
|
||||||
@@ -220,7 +156,6 @@ export async function runBuildAndSubmitAsync(
|
|||||||
platforms,
|
platforms,
|
||||||
profileName: flags.submitProfile,
|
profileName: flags.submitProfile,
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
projectDir,
|
|
||||||
});
|
});
|
||||||
for (const startedBuild of startedBuilds) {
|
for (const startedBuild of startedBuilds) {
|
||||||
const submitProfile = nullthrows(
|
const submitProfile = nullthrows(
|
||||||
@@ -236,39 +171,25 @@ export async function runBuildAndSubmitAsync(
|
|||||||
buildProfile: startedBuild.buildProfile.profile,
|
buildProfile: startedBuild.buildProfile.profile,
|
||||||
submitProfile,
|
submitProfile,
|
||||||
nonInteractive: flags.nonInteractive,
|
nonInteractive: flags.nonInteractive,
|
||||||
selectedSubmitProfileName: flags.submitProfile,
|
|
||||||
});
|
});
|
||||||
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
|
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(startedBuild.build.id);
|
||||||
graphqlClient,
|
|
||||||
startedBuild.build.id
|
|
||||||
);
|
|
||||||
submissions.push(submission);
|
submissions.push(submission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flags.localBuildOptions.localBuildMode) {
|
Log.newLine();
|
||||||
Log.newLine();
|
printSubmissionDetailsUrls(submissions);
|
||||||
printSubmissionDetailsUrls(submissions);
|
Log.newLine();
|
||||||
Log.newLine();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (flags.localBuildOptions.localBuildMode) {
|
|
||||||
return {
|
|
||||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!flags.wait) {
|
if (!flags.wait) {
|
||||||
if (flags.json) {
|
if (flags.json) {
|
||||||
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
||||||
}
|
}
|
||||||
return {
|
return;
|
||||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
||||||
const builds = await waitForBuildEndAsync(graphqlClient, {
|
const builds = await waitForBuildEndAsync({
|
||||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||||
accountName,
|
accountName,
|
||||||
});
|
});
|
||||||
@@ -277,37 +198,26 @@ export async function runBuildAndSubmitAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const haveAllBuildsFailedOrCanceled = builds.every(
|
const haveAllBuildsFailedOrCanceled = builds.every(
|
||||||
build =>
|
build => build?.status && [BuildStatus.Errored, BuildStatus.Canceled].includes(build?.status)
|
||||||
build?.status &&
|
|
||||||
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
|
|
||||||
|
|
||||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||||
if (flags.json) {
|
if (flags.json) {
|
||||||
printJsonOnlyOutput(builds);
|
printJsonOnlyOutput(builds);
|
||||||
}
|
}
|
||||||
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
exitWithNonZeroCodeIfSomeBuildsFailed(builds);
|
||||||
} else {
|
} else {
|
||||||
const completedSubmissions = await waitForSubmissionsToCompleteAsync(
|
const completedSubmissions = await waitForSubmissionsToCompleteAsync(submissions);
|
||||||
graphqlClient,
|
|
||||||
submissions
|
|
||||||
);
|
|
||||||
if (flags.json) {
|
if (flags.json) {
|
||||||
printJsonOnlyOutput(
|
printJsonOnlyOutput(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
builds
|
builds
|
||||||
.filter((i): i is BuildWithSubmissionsFragment => !!i)
|
.filter((i): i is BuildWithSubmissionsFragment => !!i)
|
||||||
.map(build => BuildQuery.withSubmissionsByIdAsync(graphqlClient, build.id))
|
.map(build => BuildQuery.withSubmissionsByIdAsync(build.id))
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function prepareAndStartBuildAsync({
|
async function prepareAndStartBuildAsync({
|
||||||
@@ -315,29 +225,19 @@ async function prepareAndStartBuildAsync({
|
|||||||
flags,
|
flags,
|
||||||
moreBuilds,
|
moreBuilds,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
|
resourceClass,
|
||||||
easJsonCliConfig,
|
easJsonCliConfig,
|
||||||
actor,
|
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
vcsClient,
|
|
||||||
getDynamicPrivateProjectConfigAsync,
|
|
||||||
customBuildConfigMetadata,
|
|
||||||
}: {
|
}: {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
flags: BuildFlags;
|
flags: BuildFlags;
|
||||||
moreBuilds: boolean;
|
moreBuilds: boolean;
|
||||||
buildProfile: ProfileData<'build'>;
|
buildProfile: ProfileData<'build'>;
|
||||||
|
resourceClass: BuildResourceClass;
|
||||||
easJsonCliConfig: EasJson['cli'];
|
easJsonCliConfig: EasJson['cli'];
|
||||||
actor: Actor;
|
|
||||||
graphqlClient: ExpoGraphqlClient;
|
|
||||||
analytics: Analytics;
|
|
||||||
vcsClient: Client;
|
|
||||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
|
||||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
|
||||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||||
const buildCtx = await createBuildContextAsync({
|
const buildCtx = await createBuildContextAsync({
|
||||||
buildProfileName: buildProfile.profileName,
|
buildProfileName: buildProfile.profileName,
|
||||||
resourceClassFlag: flags.resourceClass,
|
resourceClass,
|
||||||
clearCache: flags.clearCache,
|
clearCache: flags.clearCache,
|
||||||
buildProfile: buildProfile.profile,
|
buildProfile: buildProfile.profile,
|
||||||
nonInteractive: flags.nonInteractive,
|
nonInteractive: flags.nonInteractive,
|
||||||
@@ -347,15 +247,6 @@ async function prepareAndStartBuildAsync({
|
|||||||
localBuildOptions: flags.localBuildOptions,
|
localBuildOptions: flags.localBuildOptions,
|
||||||
easJsonCliConfig,
|
easJsonCliConfig,
|
||||||
message: flags.message,
|
message: flags.message,
|
||||||
actor,
|
|
||||||
graphqlClient,
|
|
||||||
analytics,
|
|
||||||
vcsClient,
|
|
||||||
getDynamicPrivateProjectConfigAsync,
|
|
||||||
customBuildConfigMetadata,
|
|
||||||
buildLoggerLevel: flags.buildLoggerLevel,
|
|
||||||
freezeCredentials: flags.freezeCredentials,
|
|
||||||
repack: flags.repack,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moreBuilds) {
|
if (moreBuilds) {
|
||||||
@@ -367,32 +258,6 @@ async function prepareAndStartBuildAsync({
|
|||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buildProfile.profile.channel) {
|
|
||||||
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
|
||||||
exp: buildCtx.exp,
|
|
||||||
projectId: buildCtx.projectId,
|
|
||||||
projectDir,
|
|
||||||
vcsClient: buildCtx.vcsClient,
|
|
||||||
sdkVersion: buildCtx.exp.sdkVersion,
|
|
||||||
nonInteractive: flags.nonInteractive,
|
|
||||||
buildProfile,
|
|
||||||
env: buildProfile.profile.env,
|
|
||||||
});
|
|
||||||
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
|
|
||||||
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
|
|
||||||
appId: buildCtx.projectId,
|
|
||||||
channelName: buildProfile.profile.channel,
|
|
||||||
});
|
|
||||||
if (!doesChannelExist) {
|
|
||||||
await createAndLinkChannelAsync(graphqlClient, {
|
|
||||||
appId: buildCtx.projectId,
|
|
||||||
channelName: buildProfile.profile.channel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
|
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
|
||||||
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
|
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
|
||||||
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
|
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
|
||||||
@@ -432,7 +297,6 @@ async function prepareAndStartSubmissionAsync({
|
|||||||
projectDir,
|
projectDir,
|
||||||
buildProfile,
|
buildProfile,
|
||||||
submitProfile,
|
submitProfile,
|
||||||
selectedSubmitProfileName,
|
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
}: {
|
}: {
|
||||||
build: BuildFragment;
|
build: BuildFragment;
|
||||||
@@ -441,27 +305,19 @@ async function prepareAndStartSubmissionAsync({
|
|||||||
projectDir: string;
|
projectDir: string;
|
||||||
buildProfile: BuildProfile;
|
buildProfile: BuildProfile;
|
||||||
submitProfile: SubmitProfile;
|
submitProfile: SubmitProfile;
|
||||||
selectedSubmitProfileName?: string;
|
|
||||||
nonInteractive: boolean;
|
nonInteractive: boolean;
|
||||||
}): Promise<SubmissionFragment> {
|
}): Promise<SubmissionFragment> {
|
||||||
const platform = toPlatform(build.platform);
|
const platform = toPlatform(build.platform);
|
||||||
const submissionCtx = await createSubmissionContextAsync({
|
const submissionCtx = await createSubmissionContextAsync({
|
||||||
platform,
|
platform,
|
||||||
projectDir,
|
projectDir,
|
||||||
|
projectId: build.project.id,
|
||||||
profile: submitProfile,
|
profile: submitProfile,
|
||||||
archiveFlags: { id: build.id },
|
archiveFlags: { id: build.id },
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
env: buildProfile.env,
|
env: buildProfile.env,
|
||||||
credentialsCtx: buildCtx.credentialsCtx,
|
credentialsCtx: buildCtx.credentialsCtx,
|
||||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||||
actor: buildCtx.user,
|
|
||||||
graphqlClient: buildCtx.graphqlClient,
|
|
||||||
analytics: buildCtx.analytics,
|
|
||||||
projectId: buildCtx.projectId,
|
|
||||||
exp: buildCtx.exp,
|
|
||||||
vcsClient: buildCtx.vcsClient,
|
|
||||||
isVerboseFastlaneEnabled: false,
|
|
||||||
specifiedProfile: selectedSubmitProfileName,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (moreBuilds) {
|
if (moreBuilds) {
|
||||||
@@ -484,88 +340,3 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
|
||||||
assert(build.artifacts?.applicationArchiveUrl);
|
|
||||||
const buildPath = await downloadAndMaybeExtractAppAsync(
|
|
||||||
build.artifacts.applicationArchiveUrl,
|
|
||||||
build.platform
|
|
||||||
);
|
|
||||||
await runAsync(buildPath, build.platform);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function maybeDownloadAndRunSimulatorBuildsAsync(
|
|
||||||
builds: MaybeBuildFragment[],
|
|
||||||
flags: BuildFlags
|
|
||||||
): Promise<void> {
|
|
||||||
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
|
|
||||||
|
|
||||||
if (simBuilds.length > 0 && !flags.autoSubmit && !flags.nonInteractive) {
|
|
||||||
for (const simBuild of simBuilds) {
|
|
||||||
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
|
|
||||||
Log.newLine();
|
|
||||||
const confirm = await confirmAsync({
|
|
||||||
message: `Install and run the ${
|
|
||||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
|
||||||
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
|
|
||||||
});
|
|
||||||
if (confirm) {
|
|
||||||
await downloadAndRunAsync(simBuild);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
|
||||||
exp,
|
|
||||||
projectId,
|
|
||||||
projectDir,
|
|
||||||
vcsClient,
|
|
||||||
buildProfile,
|
|
||||||
nonInteractive,
|
|
||||||
sdkVersion,
|
|
||||||
env,
|
|
||||||
}: {
|
|
||||||
exp: ExpoConfig;
|
|
||||||
projectId: string;
|
|
||||||
projectDir: string;
|
|
||||||
vcsClient: Client;
|
|
||||||
buildProfile: ProfileData<'build'>;
|
|
||||||
nonInteractive: boolean;
|
|
||||||
sdkVersion?: string;
|
|
||||||
env: Env | undefined;
|
|
||||||
}): Promise<void> {
|
|
||||||
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isExpoUpdatesInstalledAsDevDependency(projectDir)) {
|
|
||||||
Log.warn(
|
|
||||||
`The build profile "${buildProfile.profileName}" uses the channel "${buildProfile.profile.channel}", but you've added "expo-updates" as a dev dependency. To make channels work for your builds, move "expo-updates" from dev dependencies to the main dependencies in your project.`
|
|
||||||
);
|
|
||||||
} else if (nonInteractive) {
|
|
||||||
Log.warn(
|
|
||||||
`The build profile "${buildProfile.profileName}" has specified the channel "${buildProfile.profile.channel}", but the "expo-updates" package hasn't been installed. To use channels for your builds, install the "expo-updates" package by running "npx expo install expo-updates" followed by "eas update:configure".`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
Log.warn(
|
|
||||||
`The build profile "${buildProfile.profileName}" specifies the channel "${buildProfile.profile.channel}", but the "expo-updates" package is missing. To use channels in your builds, install the "expo-updates" package and run "eas update:configure".`
|
|
||||||
);
|
|
||||||
const installExpoUpdates = await confirmAsync({
|
|
||||||
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
|
|
||||||
});
|
|
||||||
if (installExpoUpdates) {
|
|
||||||
await ensureEASUpdateIsConfiguredAsync({
|
|
||||||
exp,
|
|
||||||
projectId,
|
|
||||||
projectDir,
|
|
||||||
platform: RequestedPlatform.All,
|
|
||||||
vcsClient,
|
|
||||||
env,
|
|
||||||
});
|
|
||||||
Log.withTick('Installed expo-updates and configured EAS Update.');
|
|
||||||
throw new Error('Command must be re-run to pick up new updates configuration.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ export enum BuildStatus {
|
|||||||
NEW = 'new',
|
NEW = 'new',
|
||||||
IN_QUEUE = 'in-queue',
|
IN_QUEUE = 'in-queue',
|
||||||
IN_PROGRESS = 'in-progress',
|
IN_PROGRESS = 'in-progress',
|
||||||
PENDING_CANCEL = 'pending-cancel',
|
|
||||||
ERRORED = 'errored',
|
ERRORED = 'errored',
|
||||||
FINISHED = 'finished',
|
FINISHED = 'finished',
|
||||||
CANCELED = 'canceled',
|
CANCELED = 'canceled',
|
||||||
@@ -11,6 +10,10 @@ export enum BuildStatus {
|
|||||||
export enum BuildDistributionType {
|
export enum BuildDistributionType {
|
||||||
STORE = 'store',
|
STORE = 'store',
|
||||||
INTERNAL = 'internal',
|
INTERNAL = 'internal',
|
||||||
/** @deprecated Use simulator flag instead */
|
|
||||||
SIMULATOR = 'simulator',
|
SIMULATOR = 'simulator',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum UserInputResourceClass {
|
||||||
|
DEFAULT = 'default',
|
||||||
|
LARGE = 'large',
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import { Errors } from '@oclif/core';
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import resolveFrom from 'resolve-from';
|
import resolveFrom from 'resolve-from';
|
||||||
|
|
||||||
import { reviewAndCommitChangesAsync } from './repository';
|
|
||||||
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
import { toAppPlatform } from '../../graphql/types/AppPlatform';
|
||||||
import Log, { learnMore } from '../../log';
|
import Log, { learnMore } from '../../log';
|
||||||
import { appPlatformDisplayNames } from '../../platform';
|
import { appPlatformDisplayNames } from '../../platform';
|
||||||
@@ -11,16 +10,15 @@ import { resolveWorkflowAsync } from '../../project/workflow';
|
|||||||
import { confirmAsync } from '../../prompts';
|
import { confirmAsync } from '../../prompts';
|
||||||
import { expoCommandAsync } from '../../utils/expoCli';
|
import { expoCommandAsync } from '../../utils/expoCli';
|
||||||
import { ProfileData } from '../../utils/profiles';
|
import { ProfileData } from '../../utils/profiles';
|
||||||
import { Client } from '../../vcs/vcs';
|
import { getVcsClient } from '../../vcs';
|
||||||
|
import { reviewAndCommitChangesAsync } from './repository';
|
||||||
|
|
||||||
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
||||||
projectDir,
|
projectDir,
|
||||||
vcsClient,
|
|
||||||
nonInteractive = false,
|
nonInteractive = false,
|
||||||
buildProfiles = [],
|
buildProfiles = [],
|
||||||
}: {
|
}: {
|
||||||
projectDir: string;
|
projectDir: string;
|
||||||
vcsClient: Client;
|
|
||||||
nonInteractive?: boolean;
|
nonInteractive?: boolean;
|
||||||
buildProfiles?: ProfileData<'build'>[];
|
buildProfiles?: ProfileData<'build'>[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
@@ -43,7 +41,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const workflowPerPlatformList = await Promise.all(
|
const workflowPerPlatformList = await Promise.all(
|
||||||
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform, vcsClient))
|
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
|
||||||
);
|
);
|
||||||
|
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
@@ -57,19 +55,6 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
|||||||
'expo-dev-client'
|
'expo-dev-client'
|
||||||
)} installed for your project.`
|
)} installed for your project.`
|
||||||
);
|
);
|
||||||
if (nonInteractive) {
|
|
||||||
Log.error(`You'll need to install ${chalk.bold('expo-dev-client')} manually.`);
|
|
||||||
Log.error(
|
|
||||||
learnMore('https://docs.expo.dev/clients/installation/', {
|
|
||||||
learnMoreMessage: 'See installation instructions on how to do it.',
|
|
||||||
dim: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and try again later.`, {
|
|
||||||
exit: 1,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const areAllManaged = workflowPerPlatformList.every(i => i === Workflow.MANAGED);
|
const areAllManaged = workflowPerPlatformList.every(i => i === Workflow.MANAGED);
|
||||||
if (areAllManaged) {
|
if (areAllManaged) {
|
||||||
const install = await confirmAsync({
|
const install = await confirmAsync({
|
||||||
@@ -77,7 +62,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
|
|||||||
instructions: 'The command will abort unless you agree.',
|
instructions: 'The command will abort unless you agree.',
|
||||||
});
|
});
|
||||||
if (install) {
|
if (install) {
|
||||||
await installExpoDevClientAsync(projectDir, vcsClient, { nonInteractive });
|
await installExpoDevClientAsync(projectDir, { nonInteractive });
|
||||||
} else {
|
} else {
|
||||||
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
|
||||||
exit: 1,
|
exit: 1,
|
||||||
@@ -115,7 +100,6 @@ async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolea
|
|||||||
|
|
||||||
async function installExpoDevClientAsync(
|
async function installExpoDevClientAsync(
|
||||||
projectDir: string,
|
projectDir: string,
|
||||||
vcsClient: Client,
|
|
||||||
{ nonInteractive }: { nonInteractive: boolean }
|
{ nonInteractive }: { nonInteractive: boolean }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
@@ -123,8 +107,8 @@ async function installExpoDevClientAsync(
|
|||||||
Log.newLine();
|
Log.newLine();
|
||||||
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
|
||||||
Log.newLine();
|
Log.newLine();
|
||||||
if (await vcsClient.isCommitRequiredAsync()) {
|
if (await getVcsClient().isCommitRequiredAsync()) {
|
||||||
await reviewAndCommitChangesAsync(vcsClient, 'Install expo-dev-client', {
|
await reviewAndCommitChangesAsync('Install expo-dev-client', {
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
import { getBuildLogsUrl } from './url';
|
|
||||||
import {
|
import {
|
||||||
AppPlatform,
|
AppPlatform,
|
||||||
BuildFragment,
|
BuildFragment,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
} from '../../graphql/generated';
|
} from '../../graphql/generated';
|
||||||
import { appPlatformDisplayNames } from '../../platform';
|
import { appPlatformDisplayNames } from '../../platform';
|
||||||
import formatFields from '../../utils/formatFields';
|
import formatFields from '../../utils/formatFields';
|
||||||
|
import { getBuildLogsUrl } from './url';
|
||||||
|
|
||||||
export function formatGraphQLBuild(build: BuildFragment): string {
|
export function formatGraphQLBuild(build: BuildFragment): string {
|
||||||
const actor = getActorName(build);
|
const actor = getActorName(build);
|
||||||
@@ -27,7 +27,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
|||||||
return chalk.blue('in queue');
|
return chalk.blue('in queue');
|
||||||
case GraphQLBuildStatus.InProgress:
|
case GraphQLBuildStatus.InProgress:
|
||||||
return chalk.blue('in progress');
|
return chalk.blue('in progress');
|
||||||
case GraphQLBuildStatus.PendingCancel:
|
|
||||||
case GraphQLBuildStatus.Canceled:
|
case GraphQLBuildStatus.Canceled:
|
||||||
return chalk.gray('canceled');
|
return chalk.gray('canceled');
|
||||||
case GraphQLBuildStatus.Finished:
|
case GraphQLBuildStatus.Finished:
|
||||||
@@ -39,14 +38,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Profile',
|
|
||||||
value: build.buildProfile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Message',
|
|
||||||
value: build.message,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Distribution',
|
label: 'Distribution',
|
||||||
value: build.distribution?.toLowerCase(),
|
value: build.distribution?.toLowerCase(),
|
||||||
@@ -55,6 +46,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
|||||||
label: 'Enterprise Provisioning',
|
label: 'Enterprise Provisioning',
|
||||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Release Channel',
|
||||||
|
value: build.releaseChannel,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Channel',
|
label: 'Channel',
|
||||||
value: build.channel,
|
value: build.channel,
|
||||||
@@ -91,7 +86,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
|||||||
case GraphQLBuildStatus.InQueue:
|
case GraphQLBuildStatus.InQueue:
|
||||||
case GraphQLBuildStatus.InProgress:
|
case GraphQLBuildStatus.InProgress:
|
||||||
return '<in progress>';
|
return '<in progress>';
|
||||||
case GraphQLBuildStatus.PendingCancel:
|
|
||||||
case GraphQLBuildStatus.Canceled:
|
case GraphQLBuildStatus.Canceled:
|
||||||
case GraphQLBuildStatus.Errored:
|
case GraphQLBuildStatus.Errored:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import chalk from 'chalk';
|
||||||
import indentString from 'indent-string';
|
import indentString from 'indent-string';
|
||||||
import qrcodeTerminal from 'qrcode-terminal';
|
import qrcodeTerminal from 'qrcode-terminal';
|
||||||
|
|
||||||
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
|
|
||||||
import {
|
import {
|
||||||
AppPlatform,
|
AppPlatform,
|
||||||
BuildError,
|
BuildError,
|
||||||
@@ -12,51 +12,27 @@ import {
|
|||||||
EasBuildDeprecationInfo,
|
EasBuildDeprecationInfo,
|
||||||
EasBuildDeprecationInfoType,
|
EasBuildDeprecationInfoType,
|
||||||
} from '../../graphql/generated';
|
} from '../../graphql/generated';
|
||||||
import Log, { learnMore, link } from '../../log';
|
import Log, { learnMore } from '../../log';
|
||||||
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
|
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 {
|
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||||
if (builds.length === 1) {
|
if (builds.length === 1) {
|
||||||
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
|
Log.log(`Build details: ${chalk.underline(getBuildLogsUrl(builds[0]))}`);
|
||||||
} else {
|
} else {
|
||||||
builds.forEach(build => {
|
builds.forEach(build => {
|
||||||
const logsUrl = getBuildLogsUrl(build);
|
const logsUrl = getBuildLogsUrl(build);
|
||||||
Log.log(
|
Log.log(
|
||||||
`${appPlatformEmojis[build.platform]} ${
|
`${appPlatformEmojis[build.platform]} ${
|
||||||
appPlatformDisplayNames[build.platform]
|
appPlatformDisplayNames[build.platform]
|
||||||
} build details: ${link(logsUrl)}`
|
} build details: ${chalk.underline(logsUrl)}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||||
|
Log.newLine();
|
||||||
if (builds.length === 1) {
|
if (builds.length === 1) {
|
||||||
const [build] = builds;
|
const [build] = builds;
|
||||||
assert(build, 'Build should be defined');
|
assert(build, 'Build should be defined');
|
||||||
@@ -67,8 +43,8 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printBuildResult(build: BuildFragment): void {
|
function printBuildResult(build: BuildFragment): void {
|
||||||
|
Log.addNewLineIfNone();
|
||||||
if (build.status === BuildStatus.Errored) {
|
if (build.status === BuildStatus.Errored) {
|
||||||
Log.addNewLineIfNone();
|
|
||||||
const userError = build.error;
|
const userError = build.error;
|
||||||
Log.error(
|
Log.error(
|
||||||
`${appPlatformEmojis[build.platform]} ${
|
`${appPlatformEmojis[build.platform]} ${
|
||||||
@@ -76,12 +52,11 @@ function printBuildResult(build: BuildFragment): void {
|
|||||||
} build failed${userError ? ':' : ''}`
|
} build failed${userError ? ':' : ''}`
|
||||||
);
|
);
|
||||||
if (userError) {
|
if (userError) {
|
||||||
printUserError(userError, build);
|
printUserError(userError);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ([BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build.status)) {
|
if (build.status === BuildStatus.Canceled) {
|
||||||
Log.addNewLineIfNone();
|
|
||||||
Log.error(
|
Log.error(
|
||||||
`${appPlatformEmojis[build.platform]} ${
|
`${appPlatformEmojis[build.platform]} ${
|
||||||
appPlatformDisplayNames[build.platform]
|
appPlatformDisplayNames[build.platform]
|
||||||
@@ -91,7 +66,6 @@ function printBuildResult(build: BuildFragment): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (build.distribution === DistributionType.Internal) {
|
if (build.distribution === DistributionType.Internal) {
|
||||||
Log.addNewLineIfNone();
|
|
||||||
const logsUrl = getBuildLogsUrl(build);
|
const logsUrl = getBuildLogsUrl(build);
|
||||||
// It's tricky to install the .apk file directly on Android so let's fallback
|
// 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
|
// to the build details page and let people press the button to download there
|
||||||
@@ -105,18 +79,13 @@ function printBuildResult(build: BuildFragment): void {
|
|||||||
appPlatformDisplayNames[build.platform]
|
appPlatformDisplayNames[build.platform]
|
||||||
} devices (or scan the QR code) to install the app:`
|
} devices (or scan the QR code) to install the app:`
|
||||||
);
|
);
|
||||||
Log.log(`${link(logsUrl)}`);
|
Log.log(`${chalk.underline(logsUrl)}`);
|
||||||
} else {
|
} else {
|
||||||
// TODO: it looks like buildUrl could possibly be undefined, based on the code below.
|
// 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
|
// we should account for this case better if it is possible
|
||||||
const url = build.artifacts?.buildUrl;
|
const url = build.artifacts?.buildUrl ?? '';
|
||||||
if (url) {
|
Log.log(`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`);
|
||||||
Log.addNewLineIfNone();
|
Log.log(`${chalk.underline(url)}`);
|
||||||
Log.log(
|
|
||||||
`${appPlatformEmojis[build.platform]} ${appPlatformDisplayNames[build.platform]} app:`
|
|
||||||
);
|
|
||||||
Log.log(link(url));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,22 +106,9 @@ export function printDeprecationWarnings(deprecationInfo?: EasBuildDeprecationIn
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printUserError(error: BuildError, build: BuildFragment): void {
|
export function printUserError(error: BuildError): void {
|
||||||
const maybeErrorMessageOverride = maybeGetErrorMessageOverride(error, build);
|
Log.error(error.message);
|
||||||
if (maybeErrorMessageOverride) {
|
if (error.docsUrl) {
|
||||||
Log.error(maybeErrorMessageOverride);
|
Log.error(learnMore(error.docsUrl, { dim: false }));
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
@@ -10,13 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
|
|||||||
import { formatBytes } from '../../utils/files';
|
import { formatBytes } from '../../utils/files';
|
||||||
import { getTmpDirectory } from '../../utils/paths';
|
import { getTmpDirectory } from '../../utils/paths';
|
||||||
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
|
||||||
import { Client } from '../../vcs/vcs';
|
import { getVcsClient } from '../../vcs';
|
||||||
|
|
||||||
export async function maybeBailOnRepoStatusAsync(
|
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
|
||||||
vcsClient: Client,
|
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||||
nonInteractive: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
if (!(await vcsClient.isCommitRequiredAsync())) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.addNewLineIfNone();
|
Log.addNewLineIfNone();
|
||||||
@@ -31,20 +28,12 @@ export async function maybeBailOnRepoStatusAsync(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!answer) {
|
if (!answer) {
|
||||||
if (nonInteractive) {
|
|
||||||
Log.log('The following files need to be committed:');
|
|
||||||
await vcsClient.showChangedFilesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Commit all changes. Aborting...');
|
throw new Error('Commit all changes. Aborting...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureRepoIsCleanAsync(
|
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
|
||||||
vcsClient: Client,
|
if (!(await getVcsClient().isCommitRequiredAsync())) {
|
||||||
nonInteractive = false
|
|
||||||
): Promise<void> {
|
|
||||||
if (!(await vcsClient.isCommitRequiredAsync())) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.addNewLineIfNone();
|
Log.addNewLineIfNone();
|
||||||
@@ -55,31 +44,25 @@ export async function ensureRepoIsCleanAsync(
|
|||||||
)}.`
|
)}.`
|
||||||
);
|
);
|
||||||
if (nonInteractive) {
|
if (nonInteractive) {
|
||||||
Log.log('The following files need to be committed:');
|
|
||||||
await vcsClient.showChangedFilesAsync();
|
|
||||||
|
|
||||||
throw new Error('Commit all changes. Aborting...');
|
throw new Error('Commit all changes. Aborting...');
|
||||||
}
|
}
|
||||||
const answer = await confirmAsync({
|
const answer = await confirmAsync({
|
||||||
message: `Commit changes to git?`,
|
message: `Commit changes to git?`,
|
||||||
});
|
});
|
||||||
if (answer) {
|
if (answer) {
|
||||||
await commitPromptAsync(vcsClient, { commitAllFiles: true });
|
await commitPromptAsync({ commitAllFiles: true });
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Commit all changes. Aborting...');
|
throw new Error('Commit all changes. Aborting...');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function commitPromptAsync(
|
export async function commitPromptAsync({
|
||||||
vcsClient: Client,
|
initialCommitMessage,
|
||||||
{
|
commitAllFiles,
|
||||||
initialCommitMessage,
|
}: {
|
||||||
commitAllFiles,
|
initialCommitMessage?: string;
|
||||||
}: {
|
commitAllFiles?: boolean;
|
||||||
initialCommitMessage?: string;
|
} = {}): Promise<void> {
|
||||||
commitAllFiles?: boolean;
|
|
||||||
} = {}
|
|
||||||
): Promise<void> {
|
|
||||||
const { message } = await promptAsync({
|
const { message } = await promptAsync({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'message',
|
name: 'message',
|
||||||
@@ -87,39 +70,14 @@ export async function commitPromptAsync(
|
|||||||
initial: initialCommitMessage,
|
initial: initialCommitMessage,
|
||||||
validate: (input: string) => input !== '',
|
validate: (input: string) => input !== '',
|
||||||
});
|
});
|
||||||
await vcsClient.commitAsync({
|
await getVcsClient().commitAsync({
|
||||||
commitAllFiles,
|
commitAllFiles,
|
||||||
commitMessage: message,
|
commitMessage: message,
|
||||||
nonInteractive: false,
|
nonInteractive: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LocalFile = {
|
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
|
||||||
path: string;
|
|
||||||
size: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function makeProjectMetadataFileAsync(archivePath: string): Promise<LocalFile> {
|
|
||||||
const metadataLocation = path.join(getTmpDirectory(), `${uuidv4()}-eas-build-metadata.json`);
|
|
||||||
const archiveContent: string[] = [];
|
|
||||||
|
|
||||||
await tar.list({
|
|
||||||
file: archivePath,
|
|
||||||
onentry: (entry: tar.ReadEntry) => {
|
|
||||||
if (entry.type === 'File' && !entry.path.includes('.git/')) {
|
|
||||||
archiveContent.push(entry.path);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.writeJSON(metadataLocation, {
|
|
||||||
archiveContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
return { path: metadataLocation, size: await fs.stat(metadataLocation).then(stat => stat.size) };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalFile> {
|
|
||||||
const spinner = ora('Compressing project files');
|
const spinner = ora('Compressing project files');
|
||||||
|
|
||||||
await fs.mkdirp(getTmpDirectory());
|
await fs.mkdirp(getTmpDirectory());
|
||||||
@@ -141,7 +99,7 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
|
|||||||
startTimer(compressTimerLabel);
|
startTimer(compressTimerLabel);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await vcsClient.makeShallowCopyAsync(shallowClonePath);
|
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
|
||||||
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
|
||||||
'.',
|
'.',
|
||||||
]);
|
]);
|
||||||
@@ -175,12 +133,11 @@ enum ShouldCommitChanges {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function reviewAndCommitChangesAsync(
|
export async function reviewAndCommitChangesAsync(
|
||||||
vcsClient: Client,
|
|
||||||
initialCommitMessage: string,
|
initialCommitMessage: string,
|
||||||
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
if (process.env.EAS_BUILD_AUTOCOMMIT) {
|
||||||
await vcsClient.commitAsync({
|
await getVcsClient().commitAsync({
|
||||||
commitMessage: initialCommitMessage,
|
commitMessage: initialCommitMessage,
|
||||||
commitAllFiles: false,
|
commitAllFiles: false,
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
@@ -214,11 +171,11 @@ export async function reviewAndCommitChangesAsync(
|
|||||||
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
|
||||||
);
|
);
|
||||||
} else if (selected === ShouldCommitChanges.Yes) {
|
} else if (selected === ShouldCommitChanges.Yes) {
|
||||||
await commitPromptAsync(vcsClient, { initialCommitMessage });
|
await commitPromptAsync({ initialCommitMessage });
|
||||||
Log.withTick('Committed changes.');
|
Log.withTick('Committed changes.');
|
||||||
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
|
||||||
await vcsClient.showDiffAsync();
|
await getVcsClient().showDiffAsync();
|
||||||
await reviewAndCommitChangesAsync(vcsClient, initialCommitMessage, {
|
await reviewAndCommitChangesAsync(initialCommitMessage, {
|
||||||
nonInteractive,
|
nonInteractive,
|
||||||
askedFirstTime: false,
|
askedFirstTime: false,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import { Platform } from '@expo/eas-build-job';
|
|
||||||
import { BuildProfile, ResourceClass } from '@expo/eas-json';
|
|
||||||
import chalk from 'chalk';
|
|
||||||
|
|
||||||
import { BuildResourceClass } from '../../graphql/generated';
|
|
||||||
import Log, { link } from '../../log';
|
|
||||||
|
|
||||||
type AndroidResourceClass = Exclude<
|
|
||||||
ResourceClass,
|
|
||||||
ResourceClass.M1_MEDIUM | ResourceClass.M_MEDIUM | ResourceClass.M_LARGE
|
|
||||||
>;
|
|
||||||
|
|
||||||
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
|
|
||||||
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
|
|
||||||
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
|
|
||||||
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
|
|
||||||
[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,
|
|
||||||
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 "${resourceClassFlag}" as the override.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedResourceClass = resourceClassFlag ?? profileResourceClass;
|
|
||||||
|
|
||||||
return platform === Platform.IOS
|
|
||||||
? resolveIosResourceClass(resourceClassFlag, profileResourceClass)
|
|
||||||
: resolveAndroidResourceClass(selectedResourceClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
|
|
||||||
if (selectedResourceClass && ResourceClass.M1_MEDIUM === selectedResourceClass) {
|
|
||||||
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceClass = selectedResourceClass ?? ResourceClass.DEFAULT;
|
|
||||||
|
|
||||||
return androidResourceClassToBuildResourceClassMapping[resourceClass as AndroidResourceClass];
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveIosResourceClass(
|
|
||||||
resourceClassFlag?: ResourceClass,
|
|
||||||
profileResourceClass?: ResourceClass
|
|
||||||
): BuildResourceClass {
|
|
||||||
const resourceClass = resourceClassFlag ?? profileResourceClass ?? ResourceClass.DEFAULT;
|
|
||||||
|
|
||||||
if (resourceClassFlag === ResourceClass.LARGE) {
|
|
||||||
throw new Error(
|
|
||||||
`Experimental "large" resource class for Intel iOS workers is no longer available. Remove the specified resource class to use the default, or learn more about all available resource classes: ${link(
|
|
||||||
'https://docs.expo.dev/build-reference/eas-json/'
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResourceClass.M1_MEDIUM === resourceClass) {
|
|
||||||
Log.warn(
|
|
||||||
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
|
|
||||||
'm-medium'
|
|
||||||
)} instead.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResourceClass.M_LARGE === resourceClass) {
|
|
||||||
Log.warn(
|
|
||||||
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
|
|
||||||
'large'
|
|
||||||
)} instead.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return iosResourceClassToBuildResourceClassMapping[resourceClass];
|
|
||||||
}
|
|
||||||
@@ -10,14 +10,12 @@ export function getProjectDashboardUrl(accountName: string, projectName: string)
|
|||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBuildLogsUrl(build: BuildFragment, hash?: string): string {
|
export function getBuildLogsUrl(build: BuildFragment): string {
|
||||||
const { project } = build;
|
const { project } = build;
|
||||||
const url =
|
const url =
|
||||||
project.__typename !== 'App'
|
project.__typename !== 'App'
|
||||||
? `/builds/${build.id}`
|
? `/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();
|
return new URL(url, getExpoWebsiteBaseUrl()).toString();
|
||||||
}
|
}
|
||||||
@@ -50,31 +48,3 @@ export function getUpdateGroupUrl(
|
|||||||
getExpoWebsiteBaseUrl()
|
getExpoWebsiteBaseUrl()
|
||||||
).toString();
|
).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUpdatePreviewUrl({
|
|
||||||
message,
|
|
||||||
updateRuntimeVersion,
|
|
||||||
createdAt,
|
|
||||||
slug,
|
|
||||||
projectId,
|
|
||||||
group,
|
|
||||||
}: {
|
|
||||||
message?: string;
|
|
||||||
updateRuntimeVersion: string;
|
|
||||||
createdAt: string;
|
|
||||||
slug: string;
|
|
||||||
projectId: string;
|
|
||||||
group: string;
|
|
||||||
}): string {
|
|
||||||
const previewUrl = new URL(`/preview/update`, getExpoWebsiteBaseUrl());
|
|
||||||
if (message) {
|
|
||||||
//previewUrl.searchParams.append('message', message);
|
|
||||||
}
|
|
||||||
|
|
||||||
previewUrl.searchParams.append('updateRuntimeVersion', updateRuntimeVersion);
|
|
||||||
//previewUrl.searchParams.append('createdAt', createdAt);
|
|
||||||
previewUrl.searchParams.append('slug', slug);
|
|
||||||
previewUrl.searchParams.append('projectId', projectId);
|
|
||||||
previewUrl.searchParams.append('group', group);
|
|
||||||
return previewUrl.toString();
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,9 @@ import chalk from 'chalk';
|
|||||||
import nullthrows from 'nullthrows';
|
import nullthrows from 'nullthrows';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
import { updateAppJsonConfigAsync } from './appJson';
|
|
||||||
import Log from '../../log';
|
import Log from '../../log';
|
||||||
import { promptAsync } from '../../prompts';
|
import { promptAsync } from '../../prompts';
|
||||||
|
import { updateAppJsonConfigAsync } from './appJson';
|
||||||
|
|
||||||
export function ensureStaticConfigExists(projectDir: string): void {
|
export function ensureStaticConfigExists(projectDir: string): void {
|
||||||
const paths = getConfigFilePaths(projectDir);
|
const paths = getConfigFilePaths(projectDir);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||||
import { Errors } from '@oclif/core';
|
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import semver from 'semver';
|
|
||||||
|
|
||||||
import { CommonContext } from './context';
|
|
||||||
import Log, { learnMore } from '../log';
|
import Log, { learnMore } from '../log';
|
||||||
import { isPNGAsync } from '../utils/image';
|
import { getVcsClient } from '../vcs';
|
||||||
|
import { CommonContext } from './context';
|
||||||
|
|
||||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||||
@@ -27,12 +25,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
|||||||
if (!googleServicesFilePath) {
|
if (!googleServicesFilePath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
|
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
|
||||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||||
if (
|
if (
|
||||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||||
) {
|
) {
|
||||||
Log.warn(
|
Log.warn(
|
||||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||||
@@ -52,137 +50,3 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
|||||||
function isInsideDirectory(file: string, directory: string): boolean {
|
function isInsideDirectory(file: string, directory: string): boolean {
|
||||||
return file.startsWith(directory);
|
return file.startsWith(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validatePNGsForManagedProjectAsync<T extends Platform>(
|
|
||||||
ctx: CommonContext<T>
|
|
||||||
): Promise<void> {
|
|
||||||
if (ctx.workflow !== Workflow.MANAGED) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't run PNG checks on SDK 47 and newer
|
|
||||||
// see https://github.com/expo/eas-cli/pull/1477#issuecomment-1293914917
|
|
||||||
if (!ctx.exp.sdkVersion || semver.satisfies(ctx.exp.sdkVersion, '>= 47.0.0')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.platform === Platform.ANDROID) {
|
|
||||||
await validateAndroidPNGsAsync(ctx as CommonContext<Platform.ANDROID>);
|
|
||||||
}
|
|
||||||
// Validating iOS PNGs is currently disabled
|
|
||||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
|
||||||
//
|
|
||||||
// else {
|
|
||||||
// await validateIosPNGsAsync(ctx as CommonContext<Platform.IOS>);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConfigPng = { configPath: string; pngPath: string | undefined };
|
|
||||||
|
|
||||||
async function validateAndroidPNGsAsync(ctx: CommonContext<Platform.ANDROID>): Promise<void> {
|
|
||||||
const pngs: ConfigPng[] = [
|
|
||||||
{
|
|
||||||
configPath: 'exp.icon',
|
|
||||||
pngPath: ctx.exp.icon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
configPath: 'exp.android.icon',
|
|
||||||
pngPath: ctx.exp.android?.icon,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
configPath: 'exp.android.adaptiveIcon.foregroundImage',
|
|
||||||
pngPath: ctx.exp.android?.adaptiveIcon?.foregroundImage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
configPath: 'exp.android.adaptiveIcon.backgroundImage',
|
|
||||||
pngPath: ctx.exp.android?.adaptiveIcon?.backgroundImage,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
configPath: 'exp.splash.image',
|
|
||||||
pngPath: ctx.exp.splash?.image,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
configPath: 'exp.notification.icon',
|
|
||||||
pngPath: ctx.exp.notification?.icon,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
await validatePNGsAsync(pngs);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validating iOS PNGs is currently disabled
|
|
||||||
// See https://github.com/expo/eas-cli/pull/1477 for context
|
|
||||||
//
|
|
||||||
// async function validateIosPNGsAsync(ctx: CommonContext<Platform.IOS>): Promise<void> {
|
|
||||||
// const pngs: ConfigPng[] = [
|
|
||||||
// {
|
|
||||||
// configPath: 'exp.icon',
|
|
||||||
// pngPath: ctx.exp.icon,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// configPath: 'exp.ios.icon',
|
|
||||||
// pngPath: ctx.exp.ios?.icon,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// configPath: 'exp.splash.image',
|
|
||||||
// pngPath: ctx.exp.splash?.image,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// configPath: 'exp.notification.icon',
|
|
||||||
// pngPath: ctx.exp.notification?.icon,
|
|
||||||
// },
|
|
||||||
// ];
|
|
||||||
// await validatePNGsAsync(pngs);
|
|
||||||
|
|
||||||
// const icon = ctx.exp.ios?.icon ?? ctx.exp.icon;
|
|
||||||
// if (!icon) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// const iconConfigPath = `expo${ctx.exp.ios?.icon ? '.ios' : ''}.icon`;
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// await ensurePNGIsNotTransparentAsync(icon);
|
|
||||||
// } catch (err: any) {
|
|
||||||
// if (err instanceof ImageTransparencyError) {
|
|
||||||
// Log.error(
|
|
||||||
// `Your iOS app icon (${iconConfigPath}) can't have transparency if you wish to upload your app to the Apple App Store.`
|
|
||||||
// );
|
|
||||||
// Log.error(learnMore('https://expo.fyi/remove-alpha-channel', { dim: false }));
|
|
||||||
// Errors.exit(1);
|
|
||||||
// } else {
|
|
||||||
// throw err;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
async function validatePNGsAsync(configPngs: ConfigPng[]): Promise<void> {
|
|
||||||
const validationPromises = configPngs.map(configPng => validatePNGAsync(configPng));
|
|
||||||
const validationResults = await Promise.allSettled(validationPromises);
|
|
||||||
const failedValidations = validationResults.filter(
|
|
||||||
(result): result is PromiseRejectedResult => result.status === 'rejected'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (failedValidations.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.error('PNG images validation failed:');
|
|
||||||
for (const { reason } of failedValidations) {
|
|
||||||
const error: Error = reason;
|
|
||||||
Log.error(`- ${error.message}`);
|
|
||||||
}
|
|
||||||
Errors.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validatePNGAsync({ configPath, pngPath }: ConfigPng): Promise<void> {
|
|
||||||
if (!pngPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pngPath.endsWith('.png')) {
|
|
||||||
throw new Error(`"${configPath}" is not a PNG file`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await isPNGAsync(pngPath))) {
|
|
||||||
throw new Error(`"${configPath}" is not valid PNG`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { BranchBasicInfo, ChannelBasicInfo, UpdateChannelInfoWithBranches } from '../utils';
|
|
||||||
|
|
||||||
export const testChannelBasicInfo: ChannelBasicInfo = {
|
|
||||||
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
|
|
||||||
name: 'production',
|
|
||||||
branchMapping:
|
|
||||||
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testBasicBranchInfo1: BranchBasicInfo = {
|
|
||||||
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
|
|
||||||
name: 'wrong-channel',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testBasicBranchInfo2: BranchBasicInfo = {
|
|
||||||
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
|
|
||||||
name: 'production',
|
|
||||||
};
|
|
||||||
export const channelInfoWithBranches: UpdateChannelInfoWithBranches<BranchBasicInfo> = {
|
|
||||||
...testChannelBasicInfo,
|
|
||||||
updateBranches: [testBasicBranchInfo1, testBasicBranchInfo2],
|
|
||||||
};
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { testChannelBasicInfo } from './branch-mapping-fixtures';
|
|
||||||
import {
|
|
||||||
emptyBranchMapping,
|
|
||||||
rolloutBranchMapping,
|
|
||||||
standardBranchMapping,
|
|
||||||
} from '../../rollout/__tests__/fixtures';
|
|
||||||
import {
|
|
||||||
BranchMappingValidationError,
|
|
||||||
assertVersion,
|
|
||||||
getAlwaysTrueBranchMapping,
|
|
||||||
getBranchMapping,
|
|
||||||
getEmptyBranchMapping,
|
|
||||||
getStandardBranchId,
|
|
||||||
isAlwaysTrueBranchMapping,
|
|
||||||
isEmptyBranchMapping,
|
|
||||||
} from '../branch-mapping';
|
|
||||||
|
|
||||||
describe(assertVersion, () => {
|
|
||||||
it('throws if the branch mapping is not the correct version', () => {
|
|
||||||
expect(() => assertVersion(testChannelBasicInfo, 5)).toThrowError(BranchMappingValidationError);
|
|
||||||
});
|
|
||||||
it('asserts the correct version', () => {
|
|
||||||
assertVersion(testChannelBasicInfo, 0);
|
|
||||||
expect(getBranchMapping(testChannelBasicInfo.branchMapping).version).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(getEmptyBranchMapping, () => {
|
|
||||||
it('gets an empty branch mapping', () => {
|
|
||||||
expect(isEmptyBranchMapping(getEmptyBranchMapping())).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(isEmptyBranchMapping, () => {
|
|
||||||
it('detects empty branch mappings', () => {
|
|
||||||
expect(isEmptyBranchMapping(emptyBranchMapping)).toBe(true);
|
|
||||||
expect(isEmptyBranchMapping(standardBranchMapping)).toBe(false);
|
|
||||||
expect(isEmptyBranchMapping(rolloutBranchMapping)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(isAlwaysTrueBranchMapping, () => {
|
|
||||||
it('detects always true branch mappings', () => {
|
|
||||||
expect(isAlwaysTrueBranchMapping(standardBranchMapping)).toBe(true);
|
|
||||||
expect(isAlwaysTrueBranchMapping(rolloutBranchMapping)).toBe(false);
|
|
||||||
expect(isAlwaysTrueBranchMapping(emptyBranchMapping)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(getAlwaysTrueBranchMapping, () => {
|
|
||||||
it('gets an always true branch mapping', () => {
|
|
||||||
const alwaysTrueBranchMapping = getAlwaysTrueBranchMapping('test-id');
|
|
||||||
expect(isAlwaysTrueBranchMapping(alwaysTrueBranchMapping)).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe(getStandardBranchId, () => {
|
|
||||||
it('throws if the branch mapping is not a standard mapping', () => {
|
|
||||||
expect(() => getStandardBranchId(testChannelBasicInfo)).toThrowError(
|
|
||||||
BranchMappingValidationError
|
|
||||||
);
|
|
||||||
});
|
|
||||||
it('gets a standard branch id', () => {
|
|
||||||
const channelObjectWithStandardMapping = { ...testChannelBasicInfo };
|
|
||||||
channelObjectWithStandardMapping.branchMapping = JSON.stringify(standardBranchMapping);
|
|
||||||
expect(getStandardBranchId(channelObjectWithStandardMapping)).toBe(
|
|
||||||
standardBranchMapping.data[0].branchId
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
|
|
||||||
import { UpdateBranchObject, UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
|
|
||||||
|
|
||||||
export const testUpdateBranch1: UpdateBranchObject = {
|
|
||||||
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
|
|
||||||
name: 'wrong-channel',
|
|
||||||
updateGroups: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'bbe3e63a-f620-4603-8c4d-258f3e51590d',
|
|
||||||
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
|
|
||||||
message: 'fix bug',
|
|
||||||
createdAt: '2023-07-17T22:48:59.278Z',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
platform: 'ios',
|
|
||||||
manifestFragment:
|
|
||||||
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"7d40544b395c5949f4646f5e150fe020","fileSHA256":"LN_rjlzN55dvcBL7jM5zryIpAp7iHU8VCf7RFIccbNg","storageKey":"Pi65ns2w9xLI5G_m9aGrJ5wK5FpWcGcInSSFwnA_0aA","contentType":"image/png","fileExtension":".png"},{"bundleKey":"cdd04e13d4ec83ff0cd13ec8dabdc341","fileSHA256":"irHji01LPI9giDYxtJo9bTPHaEHzuUcCMTlb4xn90Ks","storageKey":"oJZ9JX1m8IKNDgnIqcKOo5KUFOoa_NCaxruigZu65E0","contentType":"image/png","fileExtension":".png"},{"bundleKey":"a132ecc4ba5c1517ff83c0fb321bc7fc","fileSHA256":"K3RDqaWOksoRpXW9wRWYYVpX3eMFzYznDWAmBrMCzZg","storageKey":"X3OAkUu5P_LqyKFx1bNkA_qDQFLlSjCYNAlVEaOamHE","contentType":"image/png","fileExtension":".png"},{"bundleKey":"0ea69b5077e7c4696db85dbcba75b0e1","fileSHA256":"Pd0Hc-0n4j0leJ8wFzGzrEVa2L5aonoKG4OPicbSciU","storageKey":"FwJkAbrRNahBGX1KXnTAgiiwyzENu3D7DeEzCwmInyo","contentType":"image/png","fileExtension":".png"},{"bundleKey":"f5b790e2ac193b3d41015edb3551f9b8","fileSHA256":"vCfIss7SLYKt1AAi0hxjsgOMEDceLJB6ZWjQf6gTU4k","storageKey":"GQtYm7r0G9N3WJyrymjZ26p8eNMIb7rzqwSVgGco5EU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"3d0ac172fe41ab941a8c68cd7e3a17ee","fileSHA256":"jjnar-ZPrb_g0Bx5tcFfjJrP3v8HdeAlXMk__xN7I1U","storageKey":"bimf-UXq5IFH5nNHCIHdcQ2MyJwTnwDVBb-P8hr2CZw","contentType":"application/javascript","fileExtension":".bundle"}}',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
manifestPermalink: 'https://u.expo.dev/update/bbe3e63a-f620-4603-8c4d-258f3e51590d',
|
|
||||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
|
||||||
actor: {
|
|
||||||
__typename: 'User',
|
|
||||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
|
||||||
username: 'quintest113',
|
|
||||||
},
|
|
||||||
branch: {
|
|
||||||
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
|
|
||||||
name: 'wrong-channel',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
},
|
|
||||||
codeSigningInfo: null,
|
|
||||||
__typename: 'Update',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'db22401b-d321-4629-a068-21d6d1f19c02',
|
|
||||||
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
|
|
||||||
message: 'fix bug',
|
|
||||||
createdAt: '2023-07-17T22:48:59.278Z',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
platform: 'android',
|
|
||||||
manifestFragment:
|
|
||||||
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"778ffc9fe8773a878e9c30a6304784de","fileSHA256":"i2Gkx-9w3JJ1PwSUl2SC9m_UFQ7CPfx3KrZeEDc6-lU","storageKey":"idnX8z03q4vGLzhYMPaQGBADjhqvWe67_afPqJ1vGzM","contentType":"image/png","fileExtension":".png"},{"bundleKey":"376d6a4c7f622917c39feb23671ef71d","fileSHA256":"QJsG0VpGM-5viirUgoKq0M4zQ_TYyPfLhAhN5mrM54I","storageKey":"46DQ95-UdSKiRkQgt5FyXkE1FrKOT9tBZP3nS7auKlg","contentType":"image/png","fileExtension":".png"},{"bundleKey":"c79c3606a1cf168006ad3979763c7e0c","fileSHA256":"kGZm3WiTRq2iMs42ia3vkHtCV_obWT8DY40rlJf2SIQ","storageKey":"k-3xHp3vP8mR36WdccUZVPTRJKpP-zlyboWdkXCIQpQ","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02bc1fa7c0313217bde2d65ccbff40c9","fileSHA256":"_6fuRbdkBbpzkhSVAI99aMneY5X0tpQdsGNGX244IyA","storageKey":"lfqeUjiWFXwowhxVDBEzxsbTOEzYxMLebljy5Bh1mpU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"35ba0eaec5a4f5ed12ca16fabeae451d","fileSHA256":"hM9es7ICUPaeDkczvrTaX0F_BoKJKlrRteYlcHU8IbE","storageKey":"fMoMrsUeB5xWw_Ugn8FqGJf-M2pld2hkXKR704z0MM8","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"4cc2ee49988e438ac066827b809721de","fileSHA256":"ctfK72f8EdS1IQmDvJLya38RMSma16beJcMoQToZIe0","storageKey":"QdEoKuCQPvVPA71gY52wqJ9vmtV4ObnCcPE2FP4SIdo","contentType":"application/javascript","fileExtension":".bundle"}}',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
manifestPermalink: 'https://u.expo.dev/update/db22401b-d321-4629-a068-21d6d1f19c02',
|
|
||||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
|
||||||
actor: {
|
|
||||||
__typename: 'User',
|
|
||||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
|
||||||
username: 'quintest113',
|
|
||||||
},
|
|
||||||
branch: {
|
|
||||||
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
|
|
||||||
name: 'wrong-channel',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
},
|
|
||||||
codeSigningInfo: null,
|
|
||||||
__typename: 'Update',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testUpdateBranch2: UpdateBranchObject = {
|
|
||||||
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
|
|
||||||
name: 'production',
|
|
||||||
updateGroups: [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 'f04aafc1-0bf5-4548-8c35-726b467e7c40',
|
|
||||||
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
|
|
||||||
message: 'fix bug',
|
|
||||||
createdAt: '2023-06-23T23:37:10.004Z',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
platform: 'ios',
|
|
||||||
manifestFragment:
|
|
||||||
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"7d40544b395c5949f4646f5e150fe020","fileSHA256":"LN_rjlzN55dvcBL7jM5zryIpAp7iHU8VCf7RFIccbNg","storageKey":"Pi65ns2w9xLI5G_m9aGrJ5wK5FpWcGcInSSFwnA_0aA","contentType":"image/png","fileExtension":".png"},{"bundleKey":"cdd04e13d4ec83ff0cd13ec8dabdc341","fileSHA256":"irHji01LPI9giDYxtJo9bTPHaEHzuUcCMTlb4xn90Ks","storageKey":"oJZ9JX1m8IKNDgnIqcKOo5KUFOoa_NCaxruigZu65E0","contentType":"image/png","fileExtension":".png"},{"bundleKey":"a132ecc4ba5c1517ff83c0fb321bc7fc","fileSHA256":"K3RDqaWOksoRpXW9wRWYYVpX3eMFzYznDWAmBrMCzZg","storageKey":"X3OAkUu5P_LqyKFx1bNkA_qDQFLlSjCYNAlVEaOamHE","contentType":"image/png","fileExtension":".png"},{"bundleKey":"0ea69b5077e7c4696db85dbcba75b0e1","fileSHA256":"Pd0Hc-0n4j0leJ8wFzGzrEVa2L5aonoKG4OPicbSciU","storageKey":"FwJkAbrRNahBGX1KXnTAgiiwyzENu3D7DeEzCwmInyo","contentType":"image/png","fileExtension":".png"},{"bundleKey":"f5b790e2ac193b3d41015edb3551f9b8","fileSHA256":"vCfIss7SLYKt1AAi0hxjsgOMEDceLJB6ZWjQf6gTU4k","storageKey":"GQtYm7r0G9N3WJyrymjZ26p8eNMIb7rzqwSVgGco5EU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"76ec49faddaef30ba78fe50edc5ab3d6","fileSHA256":"OLUdgP-fZI2shwGS5qlIk7nWnybL_13RJl5xR85CDBU","storageKey":"OCvI0-34zApbdo3eSGfIaQUbWdZeLeKH-nJSeedKTFs","contentType":"application/javascript","fileExtension":".bundle"}}',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
manifestPermalink: 'https://u.expo.dev/update/f04aafc1-0bf5-4548-8c35-726b467e7c40',
|
|
||||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
|
||||||
actor: {
|
|
||||||
__typename: 'User',
|
|
||||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
|
||||||
username: 'quintest113',
|
|
||||||
},
|
|
||||||
branch: {
|
|
||||||
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
|
|
||||||
name: 'production',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
},
|
|
||||||
codeSigningInfo: null,
|
|
||||||
__typename: 'Update',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'd8d15786-2727-407f-bf3e-f4bda7e397d9',
|
|
||||||
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
|
|
||||||
message: 'fix bug',
|
|
||||||
createdAt: '2023-06-23T23:37:10.004Z',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
platform: 'android',
|
|
||||||
manifestFragment:
|
|
||||||
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"778ffc9fe8773a878e9c30a6304784de","fileSHA256":"i2Gkx-9w3JJ1PwSUl2SC9m_UFQ7CPfx3KrZeEDc6-lU","storageKey":"idnX8z03q4vGLzhYMPaQGBADjhqvWe67_afPqJ1vGzM","contentType":"image/png","fileExtension":".png"},{"bundleKey":"376d6a4c7f622917c39feb23671ef71d","fileSHA256":"QJsG0VpGM-5viirUgoKq0M4zQ_TYyPfLhAhN5mrM54I","storageKey":"46DQ95-UdSKiRkQgt5FyXkE1FrKOT9tBZP3nS7auKlg","contentType":"image/png","fileExtension":".png"},{"bundleKey":"c79c3606a1cf168006ad3979763c7e0c","fileSHA256":"kGZm3WiTRq2iMs42ia3vkHtCV_obWT8DY40rlJf2SIQ","storageKey":"k-3xHp3vP8mR36WdccUZVPTRJKpP-zlyboWdkXCIQpQ","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02bc1fa7c0313217bde2d65ccbff40c9","fileSHA256":"_6fuRbdkBbpzkhSVAI99aMneY5X0tpQdsGNGX244IyA","storageKey":"lfqeUjiWFXwowhxVDBEzxsbTOEzYxMLebljy5Bh1mpU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"35ba0eaec5a4f5ed12ca16fabeae451d","fileSHA256":"hM9es7ICUPaeDkczvrTaX0F_BoKJKlrRteYlcHU8IbE","storageKey":"fMoMrsUeB5xWw_Ugn8FqGJf-M2pld2hkXKR704z0MM8","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"f4c6981626dbabcf51078d07a1611f4e","fileSHA256":"q-k1hSWl49aXpSg4jOSaVVnYFSFL6xf7HVl_TQE-ObM","storageKey":"FwK1PaZqtpL0QZIgaHsxU1AV93JhMdu9K1wZizXROAc","contentType":"application/javascript","fileExtension":".bundle"}}',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
manifestPermalink: 'https://u.expo.dev/update/d8d15786-2727-407f-bf3e-f4bda7e397d9',
|
|
||||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
|
||||||
actor: {
|
|
||||||
__typename: 'User',
|
|
||||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
|
||||||
username: 'quintest113',
|
|
||||||
},
|
|
||||||
branch: {
|
|
||||||
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
|
|
||||||
name: 'production',
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
},
|
|
||||||
codeSigningInfo: null,
|
|
||||||
__typename: 'Update',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
__typename: 'UpdateBranch',
|
|
||||||
};
|
|
||||||
export const testChannelObject: UpdateChannelObject = {
|
|
||||||
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
|
|
||||||
name: 'production',
|
|
||||||
createdAt: '2023-05-16T21:26:16.946Z',
|
|
||||||
updatedAt: '2023-05-16T21:26:16.946Z',
|
|
||||||
branchMapping:
|
|
||||||
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
|
|
||||||
updateBranches: [testUpdateBranch1, testUpdateBranch2],
|
|
||||||
__typename: 'UpdateChannel',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testBasicChannelInfo: UpdateChannelBasicInfoFragment = {
|
|
||||||
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
|
|
||||||
name: 'production',
|
|
||||||
branchMapping:
|
|
||||||
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.1,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
|
|
||||||
__typename: 'UpdateChannel',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testBasicChannelInfo2: UpdateChannelBasicInfoFragment = {
|
|
||||||
id: '9d5c7bf0-d52d-474c-9140-c1bad7f0de9d',
|
|
||||||
name: 'staging',
|
|
||||||
branchMapping:
|
|
||||||
'{"data":[{"branchId":"d7d68e32-d9c9-4a8d-8d1b-21e53100a5e8","branchMappingLogic":{"operand":0.1,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"f9f708c2-0c91-4360-b2a4-0b61834aef4a","branchMappingLogic":"true"}],"version":0}',
|
|
||||||
__typename: 'UpdateChannel',
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { format } from '@expo/timeago.js';
|
|
||||||
|
|
||||||
import { testChannelObject, testUpdateBranch1, testUpdateBranch2 } from './fixtures';
|
|
||||||
import { getAlwaysTrueBranchMapping, getEmptyBranchMapping } from '../branch-mapping';
|
|
||||||
import { getDescriptionByBranchId } from '../print-utils';
|
|
||||||
|
|
||||||
describe(getDescriptionByBranchId, () => {
|
|
||||||
it('should get descriptions for multiple branches with multiple update groups', () => {
|
|
||||||
const descriptionByBranchId = getDescriptionByBranchId(testChannelObject);
|
|
||||||
expect(Object.values(descriptionByBranchId)).toHaveLength(2);
|
|
||||||
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({
|
|
||||||
branch: 'wrong-channel',
|
|
||||||
branchRolloutPercentage: 15,
|
|
||||||
update: {
|
|
||||||
codeSigningKey: undefined,
|
|
||||||
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
message: `"fix bug" (${format('2023-07-17T22:48:59.278Z', 'en_US')} by quintest113)`,
|
|
||||||
platforms: 'android, ios',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(descriptionByBranchId[testUpdateBranch2.id]).toEqual({
|
|
||||||
branch: 'production',
|
|
||||||
branchRolloutPercentage: 85,
|
|
||||||
update: {
|
|
||||||
codeSigningKey: undefined,
|
|
||||||
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
|
|
||||||
isRollBackToEmbedded: false,
|
|
||||||
message: `"fix bug" (${format('2023-06-23T23:37:10.004Z', 'en_US')} by quintest113)`,
|
|
||||||
platforms: 'android, ios',
|
|
||||||
runtimeVersion: 'exposdk:48.0.0',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should get descriptions for branches with no updates', () => {
|
|
||||||
const noUpdatesBranch = { ...testUpdateBranch1, updateGroups: [] };
|
|
||||||
const oneBranchChannel = {
|
|
||||||
...testChannelObject,
|
|
||||||
updateBranches: [noUpdatesBranch],
|
|
||||||
branchMapping: JSON.stringify(getAlwaysTrueBranchMapping(noUpdatesBranch.id)),
|
|
||||||
};
|
|
||||||
const descriptionByBranchId = getDescriptionByBranchId(oneBranchChannel);
|
|
||||||
expect(Object.values(descriptionByBranchId)).toHaveLength(1);
|
|
||||||
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({ branch: 'wrong-channel' });
|
|
||||||
});
|
|
||||||
it('should get descriptions for no branches', () => {
|
|
||||||
const noBranchChannel = {
|
|
||||||
...testChannelObject,
|
|
||||||
updateBranches: [],
|
|
||||||
branchMapping: JSON.stringify(getEmptyBranchMapping()),
|
|
||||||
};
|
|
||||||
const descriptionByBranchId = getDescriptionByBranchId(noBranchChannel);
|
|
||||||
expect(Object.values(descriptionByBranchId)).toHaveLength(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { channelInfoWithBranches, testBasicBranchInfo1 } from './branch-mapping-fixtures';
|
|
||||||
import { getUpdateBranch } from '../utils';
|
|
||||||
|
|
||||||
describe(getUpdateBranch, () => {
|
|
||||||
it('gets the update branch', () => {
|
|
||||||
expect(getUpdateBranch(channelInfoWithBranches, '754bf17f-efc0-46ab-8a59-a03f20e53e9b')).toBe(
|
|
||||||
testBasicBranchInfo1
|
|
||||||
);
|
|
||||||
expect(() => getUpdateBranch(channelInfoWithBranches, 'not-a-branch-ID')).toThrow();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { EASUpdateAction, EASUpdateContext, NonInteractiveError } from '../../eas-update/utils';
|
|
||||||
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
|
|
||||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
|
||||||
import {
|
|
||||||
Connection,
|
|
||||||
Edge,
|
|
||||||
FilterPagination,
|
|
||||||
QueryParams,
|
|
||||||
selectPaginatedAsync,
|
|
||||||
} from '../../utils/relay';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a channel for the project.
|
|
||||||
*
|
|
||||||
* @constructor
|
|
||||||
* @param {function} options.filterPredicate - A predicate to filter the channels that are shown to the user. It takes a channelInfo object as a parameter and returns a boolean.
|
|
||||||
* @param {string} options.printedType - The type of channel printed to the user. Defaults to 'channel'.
|
|
||||||
* @param {number} options.pageSize - The number of channels to show per page. Defaults to 100.
|
|
||||||
* @param {function} options.beforeEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called before each query.
|
|
||||||
* @args externalQueryParams The query params for the pagination.
|
|
||||||
* @args totalNodesFetched The total number of nodes fetched so far.
|
|
||||||
* @args dataset The dataset so far.
|
|
||||||
* @param {function} options.afterEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called after each query.
|
|
||||||
* @args externalQueryParams The query params for the pagination.
|
|
||||||
* @args totalNodesFetched The total number of nodes fetched so far.
|
|
||||||
* @args dataset The dataset so far.
|
|
||||||
* @args willFetchAgain If the query will fetch again to get a complete page.
|
|
||||||
*/
|
|
||||||
export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFragment | null> {
|
|
||||||
constructor(
|
|
||||||
private options: {
|
|
||||||
filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean;
|
|
||||||
printedType?: string;
|
|
||||||
pageSize?: number;
|
|
||||||
beforeEachFilterQuery?: (
|
|
||||||
externalQueryParams: QueryParams,
|
|
||||||
totalNodesFetched: number,
|
|
||||||
dataset: Edge<UpdateChannelBasicInfoFragment>[]
|
|
||||||
) => void;
|
|
||||||
afterEachFilterQuery?: (
|
|
||||||
externalQueryParams: QueryParams,
|
|
||||||
totalNodesFetched: number,
|
|
||||||
dataset: Edge<UpdateChannelBasicInfoFragment>[],
|
|
||||||
willFetchAgain: boolean
|
|
||||||
) => void;
|
|
||||||
} = {}
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async queryAsync(
|
|
||||||
ctx: EASUpdateContext,
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateChannelBasicInfoFragment>> {
|
|
||||||
const { graphqlClient, app } = ctx;
|
|
||||||
const { projectId } = app;
|
|
||||||
return await ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync(graphqlClient, {
|
|
||||||
appId: projectId,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async filterQueryAsync(
|
|
||||||
ctx: EASUpdateContext,
|
|
||||||
queryParams: QueryParams,
|
|
||||||
filterPredicate: (channelInfo: UpdateChannelBasicInfoFragment) => boolean
|
|
||||||
): Promise<Connection<UpdateChannelBasicInfoFragment>> {
|
|
||||||
const queryAsync = async (
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
|
|
||||||
await this.queryAsync(ctx, queryParams);
|
|
||||||
return await FilterPagination.getPageAsync({
|
|
||||||
queryParams,
|
|
||||||
queryAsync,
|
|
||||||
filterPredicate,
|
|
||||||
beforeEachQuery: this.options.beforeEachFilterQuery,
|
|
||||||
afterEachQuery: this.options.afterEachFilterQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runAsync(ctx: EASUpdateContext): Promise<UpdateChannelBasicInfoFragment | null> {
|
|
||||||
const { nonInteractive } = ctx;
|
|
||||||
const { filterPredicate } = this.options;
|
|
||||||
const printedType = this.options.printedType ?? 'channel';
|
|
||||||
const pageSize = this.options.pageSize ?? 100;
|
|
||||||
if (nonInteractive) {
|
|
||||||
throw new NonInteractiveError(
|
|
||||||
`${printedType} selection cannot be run in non-interactive mode.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryAsync = async (
|
|
||||||
queryParams: QueryParams
|
|
||||||
): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
|
|
||||||
filterPredicate
|
|
||||||
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
|
||||||
: this.queryAsync(ctx, queryParams);
|
|
||||||
|
|
||||||
const getTitleAsync = async (channelInfo: UpdateChannelBasicInfoFragment): Promise<string> =>
|
|
||||||
channelInfo.name;
|
|
||||||
|
|
||||||
return await selectPaginatedAsync({
|
|
||||||
queryAsync,
|
|
||||||
getTitleAsync,
|
|
||||||
printedType,
|
|
||||||
pageSize,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loaded 100 of 628 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user