Compare commits

..
Author SHA1 Message Date
Quinlan Jung 819f0b4c6f sdf 2023-07-18 12:52:23 -07:00
Quinlan Jung 9ffd3a096b fix test 2023-07-18 11:52:51 -07:00
Quinlan Jung e3ff26e1ca update CHANGELOG.md 2023-07-18 17:56:15 +00:00
Quinlan Jung bbd26a8030 [eas-cli] bump urql graphql client 2023-07-18 10:42:44 -07:00
635 changed files with 14061 additions and 82617 deletions

No files matched your search

-1
View File
@@ -5,4 +5,3 @@
packages/eas-cli/src/graphql/generated.ts
node_modules
ts-declarations
+6
View File
@@ -30,6 +30,12 @@ module.exports = {
},
],
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'async-protect/async-suffix': 'error',
'node/no-sync': 'error',
},
-5
View File
@@ -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.
-30
View File
@@ -1,30 +0,0 @@
commentConfiguration:
preamble: 'Subscribed to pull request'
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['douglowder']
- patterns: ['packages/eas-cli/src/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/update/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/src/commands/channel/**']
mentions: ['wschurman', 'quinlanj']
- patterns: ['packages/eas-cli/schema/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/commands/metadata/**']
mentions: ['bycedric']
- patterns: ['packages/eas-cli/src/worker/**']
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/deploy/**']
mentions: ['kitten', 'kadikraman']
- patterns: ['packages/eas-cli/src/commands/build/**']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/build/**']
mentions: ['sjchmiela']
- patterns: ['packages/eas-cli/src/commands/workflow/**']
mentions: ['sjchmiela']
+48
View File
@@ -0,0 +1,48 @@
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: 18
- name: Checkout Pull Request
run: hub 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
+3 -6
View File
@@ -8,10 +8,6 @@ on:
- labeled
- unlabeled
concurrency:
group: changelog-${{ github.ref }}
cancel-in-progress: true
jobs:
pr-changelog-entry:
name: Check CHANGELOG.md updated
@@ -20,7 +16,7 @@ jobs:
- uses: actions/checkout@v2
- name: Check if CHANGELOG.md was updated
id: changelog-updated
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
uses: tj-actions/changed-files@v35
with:
files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
@@ -36,5 +32,6 @@ jobs:
refresh-message-position: true
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!' }}
message-failure: |
❌ It looks like a changelog entry is missing for this PR. Add it manually to CHANGELOG.md.
❌ 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.
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
⏩ 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.
-14
View File
@@ -1,14 +0,0 @@
name: codemention
on:
pull_request_target:
types: [opened, synchronize, ready_for_review]
jobs:
codemention:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: tobyhs/[email protected]
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
+9 -35
View File
@@ -18,7 +18,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: true
build-linux:
name: Build for Linux
@@ -28,13 +27,9 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 22
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Ensure GraphQL schema and generated code is up-to-date
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
yarn verify-graphql-code
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
@@ -73,23 +68,17 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 22
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
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
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
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
- name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1
env:
@@ -99,15 +88,6 @@ jobs:
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }}
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:
name: Build for Windows
runs-on: ubuntu-latest
@@ -116,7 +96,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 22
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build
@@ -158,7 +138,7 @@ jobs:
with:
registry-url: 'https://registry.npmjs.org/'
scope: 'expo'
node-version: 22
node-version: 18
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
@@ -172,13 +152,7 @@ jobs:
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
echo "Run \"npm dist-tag add eas-cli@$VERSION latest-eas-build\" to promote it to production."
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog:
@@ -191,7 +165,7 @@ jobs:
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v2
with:
node-version: 22
node-version: 18
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
@@ -201,7 +175,7 @@ jobs:
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- 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:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
@@ -211,7 +185,7 @@ jobs:
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
git add CHANGELOG.md
git commit --allow-empty -m "update CHANGELOG.md"
git commit -m "update CHANGELOG.md"
git push
- name: Prepare changelog for Slack
run: |
+1 -4
View File
@@ -2,9 +2,6 @@ name: Size
on:
pull_request:
types: [synchronize, opened]
concurrency:
group: size-${{ github.ref }}
cancel-in-progress: true
jobs:
compare:
runs-on: ubuntu-latest
@@ -12,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 20
node-version: 18
- uses: preactjs/compressed-size-action@v2
with:
build-script: 'build:tarballs:linux'
-27
View File
@@ -1,27 +0,0 @@
name: Ensure GraphQL schema and generated code is up-to-date
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 8 * * *' # 8:00 AM UTC time every day
concurrency:
group: test-graphql-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
name: Ensure GraphQL schema and generated code is up-to-date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 22
- run: yarn install --frozen-lockfile
- run: yarn verify-graphql-code
working-directory: packages/eas-cli
+1 -5
View File
@@ -13,10 +13,6 @@ on:
paths:
- 'scripts/**'
concurrency:
group: test-scripts-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
@@ -25,6 +21,6 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 20
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
+3 -10
View File
@@ -6,18 +6,14 @@ on:
pull_request:
types: [opened, synchronize]
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node: ['18', '20', '22']
node: ['14', '16', '18']
include:
- node: '20'
- node: '18'
coverage: true
name: Test with Node ${{ matrix.node }}
steps:
@@ -28,9 +24,6 @@ jobs:
node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck
- run: |
git config --global user.email "[email protected]"
git config --global user.name "Test"
- run: yarn test
if: ${{ !matrix.coverage }}
env:
@@ -52,7 +45,7 @@ jobs:
notify-slack:
runs-on: ubuntu-latest
needs: [test]
needs: test
name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }}
steps:
-48
View File
@@ -1,48 +0,0 @@
on:
workflow_dispatch:
inputs:
version:
type: string
required: false
description: 'Version number for the release'
dry_run:
type: boolean
required: true
default: true
description: 'Run the release in dry run mode, without pushing any changes'
name: Manually trigger a release
jobs:
release:
name: Trigger a release
runs-on: ubuntu-latest
env:
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
INPUT_VERSION: ${{ github.event.inputs.version }}
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
steps:
- uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0
token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/setup-node@v4
with:
node-version: 22
- run: |
# Lerna will not detect changes correctly without having all tags
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
# Checkout the main branch
git checkout main
git config --global user.email "[email protected]"
git config --global user.name "Expo CI"
yarn install --frozen-lockfile --check-files
yarn build
if [[ "$INPUT_VERSION" == "" ]]; then
echo "Releasing with default version"
yarn release
else
echo "Releasing with version $INPUT_VERSION"
yarn release $INPUT_VERSION
fi
@@ -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: 'sjchmiela'
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."
})
-10
View File
@@ -1,10 +0,0 @@
{
"eslint.enable": true,
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.code-search": true,
"**/src/**/build/**": false,
"**/src/build/**": false,
}
}
-1556
View File
File diff suppressed because it is too large. Load diff
+16
View File
@@ -0,0 +1,16 @@
* @szdziedzic @khamilowicz
packages/eas-cli/src/build @szdziedzic @khamilowicz
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz
packages/eas-cli/src/submit @szdziedzic @khamilowicz
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz
packages/eas-cli/src/update @EvanBacon @byCedric @wschurman
packages/eas-cli/src/commands/update @EvanBacon @byCedric @wschurman
packages/eas-cli/schema @bycedric
packages/eas-cli/src/metadata @bycedric
packages/eas-cli/src/commands/metadata @bycedric
packages/eas-json @szdziedzic @khamilowicz
-19
View File
@@ -18,25 +18,6 @@ If you're making changes to `packages/eas-json` or prefer/need to work with prod
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.
+5 -1
View File
@@ -1,6 +1,6 @@
# Releasing EAS CLI
1. Invoke the GitHub workflow ["Manually trigger a release"](https://github.com/expo/eas-cli/actions/workflows/trigger-release.yml). The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as an input to the workflow, in semver format, e.g. "1.2.3"
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
@@ -12,3 +12,7 @@ The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version.
- Otherwise, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH version.
## Updating EAS CLI version on EAS Build
The release process promotes the new version to staging. After verifying it works there, you can promote it to production with `npm dist-tag add eas-cli@{version} latest-eas-build`.
-5
View File
@@ -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.
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
projects: [
require('./packages/eas-cli/jest.config.js'),
require('./packages/eas-json/jest.config.js'),
],
testPathIgnorePatterns: ['.*'],
};
-15
View File
@@ -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;
+7
View File
@@ -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}'],
};
-9
View File
@@ -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 -1
View File
@@ -1,5 +1,6 @@
{
"npmClient": "yarn",
"version": "16.19.3",
"useWorkspaces": true,
"version": "3.15.1",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
+14 -15
View File
@@ -10,7 +10,6 @@
"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",
"start": "lerna run watch --stream --parallel",
"start-allow-unused": "lerna run watch-allow-unused --stream --parallel",
"watch": "yarn start",
"eas": "packages/eas-cli/bin/run",
"lint": "eslint .",
@@ -20,22 +19,22 @@
"clean": "lerna run clean && rm -rf node_modules coverage"
},
"devDependencies": {
"@types/jest": "29.5.11",
"@types/node": "20.11.0",
"eslint": "8.56.0",
"eslint-config-universe": "13.0.0",
"eslint-plugin-async-protect": "3.1.0",
"@types/jest": "29.2.5",
"@types/node": "18.11.18",
"eslint": "8.31.0",
"eslint-config-universe": "11.1.1",
"eslint-plugin-async-protect": "3.0.0",
"eslint-plugin-node": "11.1.0",
"jest": "29.7.0",
"jest-watch-typeahead": "2.2.2",
"lerna": "8.2.3",
"oclif": "^3.9.0",
"prettier": "3.1.1",
"ts-jest": "29.1.1",
"typescript": "5.3.3"
"jest": "29.3.1",
"jest-watch-typeahead": "2.2.1",
"lerna": "6.4.1",
"oclif": "3.4.3",
"prettier": "2.8.2",
"ts-jest": "29.0.3",
"typescript": "4.9.4"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
"node": "18.6.0",
"yarn": "1.22.19"
}
}
@@ -1,22 +0,0 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
To use the EAS remote build provider plugin, install the `eas-build-cache-provider` package as a developer dependency:
```bash
npm install --save-dev eas-build-cache-provider
```
Then, update your **app.json** to include the `buildCacheProvider` property and its provider under `experiments`:
```json
{
"expo": {
...
"experiments": {
"buildCacheProvider": "eas"
}
}
}
```
@@ -1,58 +0,0 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "16.4.2",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@babel/code-frame": "7.23.5",
"@expo/config": "11.0.10",
"@expo/spawn-async": "^1.7.2",
"chalk": "4.1.2",
"figures": "3.2.0",
"fs-extra": "11.2.0",
"getenv": "1.0.0",
"log-symbols": "4.1.0",
"semver": "7.5.2",
"terminal-link": "2.1.1",
"tslib": "2.4.1"
},
"devDependencies": {
"@tsconfig/node18": "18.2.4",
"@types/babel__code-frame": "7.0.3",
"@types/fs-extra": "11.0.4",
"@types/getenv": "^1.0.0",
"memfs": "3.4.13",
"rimraf": "3.0.2",
"typescript": "5.3.3"
},
"engines": {
"node": ">=18.0.0"
},
"homepage": "https://github.com/expo/eas-cli",
"license": "MIT",
"main": "build/index.js",
"types": "build/index.d.ts",
"repository": "expo/eas-cli",
"scripts": {
"build": "tsc --project tsconfig.build.json",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"prepack": "yarn rebuild",
"rebuild": "rimraf build && yarn build",
"test": "jest",
"clean": "rimraf build node_modules yarn-error.log"
},
"files": [
"/build"
],
"publishConfig": {
"access": "public"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
}
}
@@ -1,19 +0,0 @@
module.exports = {
extends: ['universe/shared/typescript-analysis'],
overrides: [
{
files: ['*.ts', '*.d.ts'],
parserOptions: {
project: './packages/eas-build-cache-provider/tsconfig.json',
},
rules: {
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
},
},
],
};
@@ -1,38 +0,0 @@
import { RunOptions, getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
export function isSpawnResultError(obj: any): obj is Error & SpawnResult {
return (
obj &&
'message' in obj &&
obj.status !== undefined &&
obj.stdout !== undefined &&
obj.stderr !== undefined
);
}
export function isDevClientBuild({
runOptions,
projectRoot,
}: {
runOptions: RunOptions;
projectRoot: string;
}): boolean {
if (!hasDirectDevClientDependency(projectRoot)) {
return false;
}
if ('variant' in runOptions && runOptions.variant !== undefined) {
return runOptions.variant === 'debug';
}
if ('configuration' in runOptions && runOptions.configuration !== undefined) {
return runOptions.configuration === 'Debug';
}
return true;
}
export function hasDirectDevClientDependency(projectRoot: string): boolean {
const { dependencies = {}, devDependencies = {} } = getPackageJson(projectRoot);
return !!dependencies['expo-dev-client'] || !!devDependencies['expo-dev-client'];
}
@@ -1,136 +0,0 @@
import {
BuildCacheProviderPlugin,
CalculateFingerprintHashProps,
ResolveBuildCacheProps,
UploadBuildCacheProps,
} from '@expo/config';
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { isDevClientBuild, isSpawnResultError } from './helpers';
import Log from './log';
async function resolveBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: ResolveBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip checking for remote builds');
return null;
}
Log.log(
chalk`{whiteBright \u203A} {bold Searching builds with matching fingerprint on EAS servers}`
);
try {
const results = await spawnAsync(
'npx',
[
'eas-cli',
'build:download',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
'--non-interactive',
isDevClientBuild({ runOptions, projectRoot }) ? '--dev-client' : '--no-dev-client',
'--json',
],
{
cwd: projectRoot,
}
);
Log.log(chalk`{whiteBright \u203A} {bold Successfully downloaded cached build}`);
// {
// "path": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
return json?.path;
} catch (error) {
Log.debug('eas-cli error:', error);
// @TODO(2025-04-11): remove this in a future release
if (isSpawnResultError(error) && error.stderr.includes('command build:download not found')) {
Log.warn(
`To take advantage of EAS build cache provider, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: UploadBuildCacheProps): Promise<string | null> {
const easJsonPath = path.join(projectRoot, 'eas.json');
if (!(await fs.exists(easJsonPath))) {
Log.debug('eas.json not found, skip uploading builds');
return null;
}
try {
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to EAS}`);
const results = await spawnAsync(
'npx',
[
'eas-cli',
'upload',
`--platform=${platform}`,
`--fingerprint=${fingerprintHash}`,
buildPath ? `--build-path=${buildPath}` : '',
'--non-interactive',
'--json',
],
{
cwd: projectRoot,
}
);
// {
// "url": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
// }
const json = JSON.parse(results.stdout.trim());
Log.log(chalk`{whiteBright \u203A} {bold Build successfully uploaded: ${json?.url}}`);
return json?.url;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
async function calculateEASFingerprintHashAsync({
projectRoot,
platform,
}: CalculateFingerprintHashProps): Promise<string | null> {
// prefer using `eas fingerprint:generate` because it automatically upload sources
try {
const results = await spawnAsync(
'npx',
['eas-cli', 'fingerprint:generate', `--platform=${platform}`, '--json', '--non-interactive'],
{
cwd: projectRoot,
}
);
// {
// "hash": "203f960b965e154b77dc31c6c42e5582e8d77196"
// }
const json = JSON.parse(results.stdout.trim());
return json?.hash;
} catch (error) {
Log.debug('eas-cli error:', error);
}
return null;
}
const EASBuildCacheProvider: BuildCacheProviderPlugin = {
resolveBuildCache: resolveBuildCacheAsync,
uploadBuildCache: uploadBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASBuildCacheProvider;
@@ -1,121 +0,0 @@
import chalk from 'chalk';
import figures from 'figures';
import { boolish } from 'getenv';
import logSymbols from 'log-symbols';
import terminalLink from 'terminal-link';
type Color = (...text: string[]) => string;
export default class Log {
public static readonly isDebug = boolish('EXPO_DEBUG', false);
public static log(...args: any[]): void {
Log.consoleLog(...args);
}
public static newLine(): void {
Log.consoleLog();
}
public static addNewLineIfNone(): void {
if (!Log.isLastLineNewLine) {
Log.newLine();
}
}
public static error(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.red));
}
public static warn(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
}
public static debug(...args: any[]): void {
if (Log.isDebug) {
Log.consoleLog(...args);
}
}
public static gray(...args: any[]): void {
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
}
public static warnDeprecatedFlag(flag: string, message: string): void {
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
}
public static fail(message: string): void {
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
}
public static succeed(message: string): void {
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
}
public static withTick(...args: any[]): void {
Log.consoleLog(chalk.green(figures.tick), ...args);
}
public static withInfo(...args: any[]): void {
Log.consoleLog(chalk.green(figures.info), ...args);
}
private static consoleLog(...args: any[]): void {
Log.updateIsLastLineNewLine(args);
// eslint-disable-next-line no-console
console.log(...args);
}
private static withTextColor(args: any[], chalkColor: Color): string[] {
return args.map(arg => chalkColor(arg));
}
private static isLastLineNewLine = false;
private static updateIsLastLineNewLine(args: any[]): void {
if (args.length === 0) {
Log.isLastLineNewLine = true;
} else {
const lastArg = args[args.length - 1];
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
Log.isLastLineNewLine = true;
} else {
Log.isLastLineNewLine = false;
}
}
}
}
/**
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
* Format links as dim (unless disabled) and with an underline.
*
* @example https://expo.dev
*/
export function link(
url: string,
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
): string {
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
const output = terminalLink(text, url, {
fallback: () =>
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
});
return dim ? chalk.dim(output) : output;
}
/**
* Provide a consistent "Learn more" link experience.
* Format links as dim (unless disabled) with an underline.
*
* @example Learn more: https://expo.dev
*/
export function learnMore(
url: string,
{
learnMoreMessage: maybeLearnMoreMessage,
dim = true,
}: { learnMoreMessage?: string; dim?: boolean } = {}
): string {
return link(url, { text: maybeLearnMoreMessage ?? 'Learn more', dim });
}
@@ -1,4 +0,0 @@
{
"extends": "./tsconfig",
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
}
@@ -1,18 +0,0 @@
{
"extends": "@tsconfig/node18/tsconfig.json",
"compilerOptions": {
"declaration": true,
"importHelpers": true,
"module": "commonjs",
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "build",
"rootDir": "src",
"typeRoots": ["../../node_modules/@types", "../../ts-declarations", "./node_modules/@types"]
},
"include": ["src/**/*"]
}
+212 -1213
View File
File diff suppressed because it is too large. Load diff
-3
View File
@@ -12,6 +12,3 @@ if (process.env.TMPDIR) {
(fs.realpath as any).native = jest.fn();
module.exports = fs;
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
jest.mock('node:fs', () => fs);
+6 -1
View File
@@ -1,7 +1,12 @@
overwrite: true
schema: 'https://staging-api.expo.dev/graphql'
documents:
- 'src/**/!(*.d).ts'
- 'src/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
- 'src/commands/**/*.ts'
- 'src/branch/**/*.ts'
- 'src/channel/**/*.ts'
generates:
src/graphql/generated.ts:
plugins:
+1993 -29398
View File
File diff suppressed because it is too large. Load diff
+7
View File
@@ -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'],
};
-14
View File
@@ -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;
+61 -83
View File
@@ -1,138 +1,125 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "16.19.3",
"version": "3.15.1",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "2.1.12",
"@expo/apple-utils": "1.3.1",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6",
"@expo/config-plugins": "9.0.12",
"@expo/eas-build-job": "1.0.221",
"@expo/eas-json": "16.19.1",
"@expo/env": "^1.0.0",
"@expo/json-file": "8.3.3",
"@expo/logger": "1.0.221",
"@expo/multipart-body-parser": "2.0.0",
"@expo/osascript": "2.1.4",
"@expo/package-manager": "1.7.0",
"@expo/pkcs12": "0.1.3",
"@expo/plist": "0.2.0",
"@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "8.0.17",
"@expo/config": "8.1.2",
"@expo/config-plugins": "7.2.4",
"@expo/config-types": "49.0.0",
"@expo/eas-build-job": "1.0.13",
"@expo/eas-json": "3.15.1",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "0.0.57",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.20",
"@expo/plugin-help": "5.1.22",
"@expo/plugin-warn-if-update-available": "2.5.0",
"@expo/prebuild-config": "6.2.5",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2",
"@expo/steps": "1.0.221",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.25",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10",
"@oclif/core": "1.23.2",
"@oclif/plugin-autocomplete": "1.3.10",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "4.0.11",
"@urql/exchange-graphcache": "6.1.4",
"@urql/exchange-retry": "1.2.0",
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
"bplist-parser": "^0.3.0",
"chalk": "4.1.2",
"cli-progress": "3.12.0",
"cli-progress": "3.11.2",
"dateformat": "4.6.3",
"diff": "7.0.0",
"dotenv": "16.3.1",
"dotenv": "16.0.3",
"env-paths": "2.2.0",
"envinfo": "7.11.0",
"envinfo": "7.8.1",
"fast-deep-equal": "3.1.3",
"fast-glob": "3.3.2",
"fast-glob": "3.2.12",
"figures": "3.2.0",
"form-data": "^4.0.4",
"fs-extra": "11.2.0",
"form-data": "4.0.0",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.1",
"graphql": "16.8.1",
"graphql": "16.6.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.3.0",
"ignore": "5.2.4",
"indent-string": "4.0.0",
"jks-js": "1.1.0",
"joi": "17.11.0",
"jsonwebtoken": "9.0.0",
"keychain": "1.5.0",
"joi": "17.7.0",
"keychain": "1.3.0",
"log-symbols": "4.1.0",
"mime": "3.0.0",
"minimatch": "5.1.2",
"minizlib": "3.0.1",
"nanoid": "3.3.8",
"nanoid": "3.3.4",
"node-fetch": "2.6.7",
"node-forge": "1.3.1",
"node-stream-zip": "1.15.0",
"nullthrows": "1.1.1",
"ora": "5.1.0",
"pkg-dir": "4.2.0",
"pngjs": "7.0.0",
"pngjs": "6.0.0",
"promise-limit": "2.7.0",
"promise-retry": "2.0.1",
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.5.4",
"set-interval-async": "3.0.3",
"semver": "7.3.8",
"slash": "3.0.0",
"tar": "6.2.1",
"tar-stream": "3.1.7",
"tar": "6.1.13",
"terminal-link": "2.1.1",
"tslib": "2.6.2",
"turndown": "7.1.2",
"tslib": "2.4.1",
"turndown": "7.1.1",
"untildify": "4.0.0",
"uuid": "9.0.1",
"wrap-ansi": "7.0.0",
"yaml": "2.6.0",
"zod": "^4.1.3"
"uuid": "9.0.0",
"wrap-ansi": "7.0.0"
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@tsconfig/node18": "18.2.4",
"@types/cli-progress": "3.11.5",
"@graphql-codegen/cli": "2.16.3",
"@graphql-codegen/introspection": "2.2.3",
"@graphql-codegen/typescript": "2.8.7",
"@graphql-codegen/typescript-operations": "2.5.12",
"@types/cli-progress": "3.11.0",
"@types/dateformat": "3.0.1",
"@types/diff": "6.0.0",
"@types/envinfo": "7.8.3",
"@types/envinfo": "7.8.1",
"@types/express": "4.17.15",
"@types/fs-extra": "11.0.4",
"@types/fs-extra": "9.0.13",
"@types/getenv": "^1.0.0",
"@types/jsonwebtoken": "8.5.1",
"@types/mime": "3.0.1",
"@types/node-fetch": "2.6.12",
"@types/node-fetch": "2.6.2",
"@types/node-forge": "1.3.1",
"@types/pngjs": "6.0.4",
"@types/pngjs": "6.0.1",
"@types/promise-retry": "1.1.3",
"@types/prompts": "2.4.2",
"@types/semver": "7.5.6",
"@types/tar": "6.1.10",
"@types/tar-stream": "3.1.3",
"@types/semver": "7.3.13",
"@types/tar": "6.1.3",
"@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7",
"@types/uuid": "9.0.0",
"@types/wrap-ansi": "3.0.0",
"axios": "1.11.0",
"axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0",
"express": "4.20.0",
"express": "4.18.2",
"memfs": "3.4.13",
"mockdate": "3.0.5",
"nock": "13.4.0",
"nock": "13.2.9",
"rimraf": "3.0.2",
"ts-deepmerge": "6.2.0",
"ts-deepmerge": "4.0.0",
"ts-mockito": "2.6.1",
"ts-node": "10.9.2",
"typescript": "5.3.3"
"ts-node": "10.9.1",
"typescript": "4.9.4"
},
"engines": {
"node": ">=18.0.0"
"node": ">=14.0.0"
},
"files": [
"/bin",
@@ -183,22 +170,16 @@
"secret": {
"description": "manage project and account secrets"
},
"env": {
"description": "manage project and account environment variables"
},
"update": {
"description": "manage individual updates"
},
"webhook": {
"description": "manage webhooks"
},
"workflow": {
"description": "manage workflows"
}
},
"warn-if-update-available": {
"timeoutInDays": 0.5,
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run:') %>\n<%= chalk.dim(chalk.bold('npm install -g eas-cli')) %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
},
"update": {
"node": {
@@ -227,18 +208,15 @@
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
"watch": "yarn build --watch --preserveWatchOutput",
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
"verify-graphql-code": "./scripts/verify-graphql.sh",
"clean": "rimraf dist build tmp node_modules yarn-error.log"
},
"volta": {
"node": "20.11.0",
"yarn": "1.22.21"
"node": "18.6.0",
"yarn": "1.22.19"
}
}
+18 -53
View File
@@ -1,20 +1,6 @@
{
"$schema": "http://json-schema.org/draft-06/schema",
"type": "object",
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
],
"definitions": {
"apple": {
"AppleKidsAge": {
@@ -337,8 +323,7 @@
"gambling",
"unrestrictedWebAccess",
"kidsAgeBand",
"ageRatingOverride",
"koreaAgeRatingOverride"
"seventeenPlus"
],
"defaultSnippets": [
{
@@ -360,9 +345,7 @@
"gambling": false,
"unrestrictedWebAccess": false,
"kidsAgeBand": null,
"ageRatingOverride": "NONE",
"koreaAgeRatingOverride": "NONE",
"lootBox": false
"seventeenPlus": false
}
},
{
@@ -384,9 +367,7 @@
"unrestrictedWebAccess": false,
"gambling": false,
"kidsAgeBand": null,
"ageRatingOverride": "${13|NONE,SEVENTEEN_PLUS,UNRATED|}",
"koreaAgeRatingOverride": "${14|NONE,FIFTEEN_PLUS,NINETEEN_PLUS|}",
"lootBox": false
"seventeenPlus": false
}
}
],
@@ -461,24 +442,8 @@
},
"seventeenPlus": {
"type": "boolean",
"deprecated": true,
"deprecationMessage": "This property is deprecated, use `ageRatingOverride` instead.",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"ageRatingOverride": {
"$ref": "#/definitions/apple/AppleAgeRatingOverride",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"koreaAgeRatingOverride": {
"$ref": "#/definitions/apple/AppleKoreaAgeRatingOverride",
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
},
"lootBox": {
"type": "boolean",
"description": "Does your app contain purchasable loot boxes? These virtual containers provide players with in-game items, including functional cards or cosmetic modifications, based on random chance."
}
}
},
@@ -600,20 +565,6 @@
"maxLength": 4000
}
}
},
"AppleAgeRatingOverride": {
"enum": [
"NONE",
"SEVENTEEN_PLUS",
"UNRATED"
]
},
"AppleKoreaAgeRatingOverride": {
"enum": [
"NONE",
"FIFTEEN_PLUS",
"NINETEEN_PLUS"
]
}
},
"AppleConfig": {
@@ -824,5 +775,19 @@
}
}
}
}
},
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
]
}
+2 -1
View File
@@ -3,7 +3,8 @@ const fs = require('fs/promises');
(async () => {
// Patch `oclif readme` path and link generation
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);
// eslint-disable-next-line no-console
@@ -1,37 +0,0 @@
#!/usr/bin/env bash
function cleanup()
{
echo 'Cleaning up...'
rm -f graphql.schema.json.bak src/graphql/generated.ts.bak
}
# Fail if anything errors
set -eox pipefail
# If this script exits, trap it first and clean up
trap cleanup EXIT
cp graphql.schema.json graphql.schema.json.bak
cp src/graphql/generated.ts src/graphql/generated.ts.bak
yarn generate-graphql-code
if cmp -s graphql.schema.json graphql.schema.json.bak; then
echo "GraphQL schema is up-to-date"
if cmp -s src/graphql/generated.ts src/graphql/generated.ts.bak; then
echo "GraphQL generated code is up-to-date"
exit 0
else
echo "GraphQL code has changed but has not been regenerated. Run `yarn generate-graphql-code` and commit the changes."
exit 1
fi
else
echo "GraphQL schema has changed on the server."
echo "Building eas-json"
cd ../eas-json
yarn build
cd ../eas-cli
echo "Running `yarn tsc` to check for breaking changes..."
yarn tsc
# If there are no errors, then the schema is backwards compatible
exit 0
fi
-40
View File
@@ -1,40 +0,0 @@
module.exports = {
extends: ['universe/shared/typescript-analysis'],
overrides: [
{
files: ['*.ts', '*.d.ts'],
parserOptions: {
project: './packages/eas-cli/tsconfig.json',
},
rules: {
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'@typescript-eslint/prefer-nullish-coalescing': ['warn', { ignorePrimitives: true }],
'@typescript-eslint/no-confusing-void-expression': 'warn',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
{
checksVoidReturn: false,
},
],
'@typescript-eslint/no-floating-promises': 'error',
'no-void': ['warn', { allowAsStatement: true }],
'no-return-await': 'off',
'@typescript-eslint/return-await': ['error', 'always'],
'@typescript-eslint/no-confusing-non-null-assertion': 'warn',
'@typescript-eslint/no-extra-non-null-assertion': 'warn',
'@typescript-eslint/prefer-as-const': 'warn',
'@typescript-eslint/prefer-includes': 'warn',
'@typescript-eslint/prefer-readonly': 'warn',
'@typescript-eslint/prefer-string-starts-ends-with': 'warn',
'@typescript-eslint/prefer-ts-expect-error': 'warn',
'@typescript-eslint/no-unnecessary-type-assertion': 'warn',
},
},
],
};
-3
View File
@@ -6,7 +6,6 @@ export class ApiV2Error extends Error {
readonly expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object;
readonly expoApiV2ErrorRequestId?: string;
constructor(response: {
message: string;
@@ -14,13 +13,11 @@ export class ApiV2Error extends Error {
stack?: string;
details?: JSONValue;
metadata?: object;
requestId?: string;
}) {
super(response.message);
this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata;
this.expoApiV2ErrorRequestId = response.requestId;
}
}
@@ -16,7 +16,6 @@ describe(ApiV2Client, () => {
stack: 'line 1: hello',
details: { who: 'world' },
metadata: { an: 'object' },
requestId: 'request-id',
},
],
});
@@ -38,7 +37,6 @@ describe(ApiV2Client, () => {
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
});
it('does not convert non-APIv2 error to ApiV2Error', async () => {
@@ -1,5 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BuildVersionGetView reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"Aborting..."`;
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add {"cli": { "appVersionSource": "remote" }} in eas.json or re-run this command without "--non-interactive" flag."`;
@@ -1,26 +1,27 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk';
import {
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import BuildVersionGetView from '../../commands/build/version/get';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
jest.mock('../../project/applicationIdentifier');
jest.mock('fs');
jest.mock('../../log');
jest.mock('../../utils/json');
jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../prompts');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionGetView, () => {
afterEach(() => {
@@ -48,29 +49,6 @@ describe(BuildVersionGetView, () => {
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('reading version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
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()),
@@ -167,51 +145,6 @@ describe(BuildVersionGetView, () => {
});
test('reading version when appVersionSource is set to local ', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
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();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
test('reading version aborts when the appVersionSource is not specified and the user chooses to configure it manually', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
storeVersion: '1.0.0',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
});
test('reading version sets appVersionSource to LOCAL and aborts when the appVersionSource is not specified and is run in non-interactive mode', async () => {
const ctx = mockCommandContext(BuildVersionGetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100',
@@ -223,10 +156,6 @@ describe(BuildVersionGetView, () => {
['--non-interactive', '--json', '--platform=android'],
ctx
);
await expect(cmd.run()).rejects.toThrowError(
`This project is not configured for using remote version source. Add ${chalk.bold(
'{"cli": { "appVersionSource": "remote" }}'
)} in eas.json or re-run this command without "--non-interactive" flag.`
);
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
});
});
@@ -1,22 +1,20 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra';
import path from 'path';
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';
import {
getMockAppFragment,
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} 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 { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
jest.mock('../../project/applicationIdentifier');
jest.mock('../../graphql/queries/AppVersionQuery');
@@ -27,6 +25,16 @@ 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();
@@ -59,34 +67,6 @@ describe(BuildVersionSetView, () => {
);
});
test('setting version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
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()),
@@ -135,9 +115,7 @@ describe(BuildVersionSetView, () => {
});
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
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);
@@ -147,24 +125,8 @@ describe(BuildVersionSetView, () => {
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
test('setting version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they 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.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(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, {
easJson: withLocalVersionSource(getMockEasJson()),
});
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);
@@ -175,33 +137,4 @@ describe(BuildVersionSetView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('setting version when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementationOnce(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');
});
test('setting version aborts when the appVersionSource is not specified and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSetView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,16 +1,8 @@
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,
withLocalVersionSource,
withRemoteVersionSource,
} 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';
@@ -20,9 +12,15 @@ 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 { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../project/workflow';
import * as prompts from '../../prompts';
import {
getMockAppFragment,
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
jest.mock('../../build/android/version');
jest.mock('../../build/ios/version');
@@ -39,6 +37,16 @@ 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();
@@ -71,34 +79,6 @@ describe(BuildVersionSyncView, () => {
expect(syncAndroidAsync).not.toHaveBeenCalled();
});
test('syncing version for managed project on platform android when appVersionSource is not set and the user chooses to set it to REMOTE', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '1000',
storeVersion: '0.0.1',
}));
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
version: '1000',
}));
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
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()),
@@ -199,25 +179,9 @@ describe(BuildVersionSyncView, () => {
});
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
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 aborts when appVersionSource is not set and the user chooses to set it to LOCAL, and they 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.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
@@ -226,9 +190,7 @@ describe(BuildVersionSyncView, () => {
});
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withLocalVersionSource(getMockEasJson()),
});
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);
@@ -239,33 +201,4 @@ describe(BuildVersionSyncView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
});
test('syncing version when appVersionSource is not set and the user chooses to set it to LOCAL and they allow auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
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');
});
test('syncing version aborts when appVersionSource is not set and the user chooses to configure manually', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
await expect(cmd.run()).rejects.toThrowError('Aborted.');
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
});
});
@@ -1,5 +1,5 @@
import { ExpoConfig } from '@expo/config';
import { AppVersionSource, EasJson } from '@expo/eas-json';
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';
@@ -7,16 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
import {
AppFragment,
Role,
WorkflowJobStatus,
WorkflowJobType,
WorkflowRunByIdWithJobsQuery,
WorkflowRunFragment,
WorkflowRunStatus,
WorkflowRunTriggerEventType,
} from '../../graphql/generated';
import { AppFragment, Role } from '../../graphql/generated';
export function getMockEasJson(): EasJson {
return {
@@ -33,7 +24,6 @@ export function getMockAppFragment(): AppFragment {
return {
id: mockProjectId,
slug: 'testapp',
name: 'testapp',
fullName: '@testuser/testpp',
ownerAccount: {
id: 'test-account-id',
@@ -69,7 +59,7 @@ export interface LocalProjectContext {
export function mockCommandContext<
C extends {
[name: string]: any;
} = object,
} = object
>(
commandClass: { contextDefinition: ContextInput<C> },
overrides: {
@@ -90,9 +80,7 @@ export function mockCommandContext<
if (contextKey === 'loggedIn') {
result.loggedIn = {};
}
if (contextKey === 'projectId') {
result.projectId = overrides.projectId ?? mockProjectId;
}
if (contextKey === 'projectDir') {
result.projectDir = projectDir;
}
@@ -159,150 +147,3 @@ export const getError = <TError = any>(call: () => unknown): TError | NoErrorThr
return error as TError;
}
};
export function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
export function withLocalVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.LOCAL,
},
};
}
export function getMockEmptyWorkflowRunsFragment(): WorkflowRunFragment[] {
return getMockWorkflowRunsFragment();
}
export function getMockWorkflowRunsFragment(
params?:
| undefined
| {
successes?: number;
failures?: number;
pending?: number;
withJobs?: number;
}
): WorkflowRunFragment[] {
const { successes = 0, failures = 0, pending = 0, withJobs = 0 } = params ?? {};
const runs: any[] = [];
for (let i = 0; i < successes; i++) {
runs.push({
id: `success-${i}`,
status: WorkflowRunStatus.Success,
errors: [],
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < failures; i++) {
runs.push({
id: `failure-${i}`,
errors: ['Failed'],
status: WorkflowRunStatus.Failure,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < pending; i++) {
runs.push({
id: `pending-${i}`,
errors: [],
status: WorkflowRunStatus.InProgress,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
name: 'build',
id: 'build',
fileName: 'build.yml',
},
});
}
for (let i = 0; i < withJobs; i++) {
runs.push(getMockWorkflowRunWithJobsFragment());
}
return runs;
}
export function getMockWorkflowJobFragment(
runId?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
return {
id: 'job1',
key: 'job1',
name: 'job1',
status: WorkflowJobStatus.Success,
type: WorkflowJobType.Build,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
outputs: {},
errors: [],
workflowRun: {
id: runId ?? 'build1',
},
turtleJobRun: {
id: 'job1',
logFileUrls: ['https://example.com/log1'],
errors: [],
artifacts: [],
},
};
}
export function getMockWorkflowRunWithJobsFragment(
runID?: string
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
const id = runID ?? 'build1';
return {
id,
status: WorkflowRunStatus.Failure,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
errors: [],
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
app: {
id: mockProjectId,
__typename: 'App',
name: 'App',
ownerAccount: {
id: 'account-id',
name: 'account-name',
__typename: 'Account',
},
},
},
jobs: [getMockWorkflowJobFragment(id)],
};
}
@@ -1,188 +0,0 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowJobFragment,
getMockWorkflowRunWithJobsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import { fetchRawLogsForJobAsync } from '../../commandUtils/workflow/fetchLogs';
import WorkflowLogView from '../../commands/workflow/logs';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import Log from '../../log';
import { promptAsync } 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/queries/WorkflowJobQuery');
jest.mock('../../graphql/queries/WorkflowRunQuery');
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');
jest.mock('../../commandUtils/workflow/fetchLogs');
describe(WorkflowLogView, () => {
beforeEach(() => {
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowJobFragment());
jest
.mocked(WorkflowRunQuery.withJobsByIdAsync)
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
});
afterEach(() => {
jest.clearAllMocks();
});
test('view logs, passing in no parameters, no runs found', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(Log.error).toHaveBeenCalledWith('No workflow runs found');
});
test('view logs, passing in no parameters, runs found, no jobs', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ successes: 1 }));
jest.mocked(promptAsync).mockResolvedValue({ selectedRun: 'build1' });
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(Log.error).toHaveBeenCalledWith('No job found');
});
test('view logs, passing in no parameters, runs found, jobs found, step selected, no logs', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
let promptCalls = 0;
jest.mocked(promptAsync).mockImplementation(async () => {
if (promptCalls === 0) {
promptCalls++;
return { selectedRun: 'build1' };
} else if (promptCalls === 1) {
promptCalls++;
return { selectedJob: 0 };
} else {
return { selectedStep: 'step1' };
}
});
jest.mocked(fetchRawLogsForJobAsync).mockResolvedValue(null);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(promptAsync).toHaveBeenCalledTimes(2);
expect(Log.error).toHaveBeenCalledWith('No logs found');
});
test('view logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
let promptCalls = 0;
jest.mocked(promptAsync).mockImplementation(async () => {
if (promptCalls === 0) {
promptCalls++;
return { selectedRun: 'build1' };
} else if (promptCalls === 1) {
promptCalls++;
return { selectedJob: 0 };
} else {
return { selectedStep: 'step1' };
}
});
jest
.mocked(fetchRawLogsForJobAsync)
.mockResolvedValue(
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
20
);
expect(promptAsync).toHaveBeenCalledTimes(3);
expect(Log.error).not.toHaveBeenCalled();
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
});
test('view logs for a workflow job, passing in a job ID, all steps', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
const cmd = mockTestCommand(WorkflowLogView, ['job1', '--all-steps'], ctx);
await cmd.run();
expect(WorkflowJobQuery.byIdAsync).toHaveBeenCalledWith(ctx.loggedIn.graphqlClient, 'job1', {
useCache: false,
});
});
test('view logs for a workflow job, passing in a run ID', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--all-steps'], ctx);
await cmd.run();
expect(WorkflowRunQuery.withJobsByIdAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
'build1',
{
useCache: false,
}
);
});
test('view logs for a workflow job, passing in a run ID, non-interactive', async () => {
const ctx = mockCommandContext(WorkflowLogView, {
projectId: mockProjectId,
});
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--non-interactive'], ctx);
try {
await cmd.run();
} catch (e) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toEqual('No workflow job found that matched the provided ID');
}
});
});
@@ -1,184 +0,0 @@
import {
getMockEmptyWorkflowRunsFragment,
getMockWorkflowRunsFragment,
mockCommandContext,
mockProjectId,
mockTestCommand,
} from './utils';
import WorkflowRunList from '../../commands/workflow/runs';
import { WorkflowRunStatus, WorkflowRunTriggerEventType } from '../../graphql/generated';
import { AppQuery } from '../../graphql/queries/AppQuery';
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
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/queries/WorkflowRunQuery');
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');
describe(WorkflowRunList, () => {
afterEach(() => {
jest.clearAllMocks();
});
test('list workflow runs with default params', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, [], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with custom limit', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
const cmd = mockTestCommand(WorkflowRunList, ['--limit', '100'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
undefined,
100
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list workflow runs with specific filename', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build',
status: WorkflowRunStatus.Success,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
errors: [],
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(WorkflowRunList, ['--workflow', 'build.yml'], ctx);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
undefined,
10
);
expect(enableJsonOutput).not.toHaveBeenCalled();
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
});
test('list only workflow runs with status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
.mockResolvedValue(getMockWorkflowRunsFragment({ failures: 1 }));
const cmd = mockTestCommand(WorkflowRunList, ['--status', 'FAILURE', '--json'], ctx);
await cmd.run();
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'failure-0',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
trigger: '',
triggerType: 'Other',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
test('list only workflow runs with specific filename and status FAILURE, get json output', async () => {
const ctx = mockCommandContext(WorkflowRunList, {
projectId: mockProjectId,
});
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
{
id: 'build1',
status: WorkflowRunStatus.Failure,
triggerEventType: WorkflowRunTriggerEventType.Manual,
createdAt: '2022-01-01T00:00:00.000Z',
updatedAt: '2022-01-01T00:00:00.000Z',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
errors: [],
workflow: {
id: 'build',
name: 'build',
fileName: 'build.yml',
},
},
]);
const cmd = mockTestCommand(
WorkflowRunList,
['--workflow', 'build.yml', '--status', 'FAILURE', '--json'],
ctx
);
await cmd.run();
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
ctx.loggedIn.graphqlClient,
mockProjectId,
'build.yml',
WorkflowRunStatus.Failure,
10
);
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
{
id: 'build1',
status: 'FAILURE',
gitCommitHash: '1234567890',
gitCommitMessage: 'commit message',
trigger: '',
triggerType: 'Manual',
startedAt: '2022-01-01T00:00:00.000Z',
finishedAt: '2022-01-01T00:00:00.000Z',
workflowId: 'build',
workflowName: 'build',
workflowFileName: 'build.yml',
},
]);
});
});
+1 -5
View File
@@ -95,11 +95,7 @@ export function getExpoWebsiteBaseUrl(): string {
}
}
export function getEASUpdateURL(projectId: string, manifestHostOverride: string | null): string {
if (manifestHostOverride) {
return new URL(projectId, `https://${manifestHostOverride}`).href;
}
export function getEASUpdateURL(projectId: string): string {
if (process.env.EXPO_STAGING) {
return new URL(projectId, `https://staging-u.expo.dev`).href;
} else if (process.env.EXPO_LOCAL) {
@@ -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 readonly 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
? await this.filterQueryAsync(ctx, queryParams, filterPredicate)
: await 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);
});
});
+10 -15
View File
@@ -1,19 +1,16 @@
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { BranchNotFoundError } from './utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
import {
CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables,
UpdateBranchBasicInfoFragment,
UpdateBranch,
UpdateBranchFragment,
} from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery';
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json';
@@ -22,6 +19,7 @@ import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { BranchNotFoundError } from './utils';
export const BRANCHES_LIMIT = 50;
@@ -86,9 +84,7 @@ export async function listAndRenderBranchesOnAppAsync(
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: {
title: 'Load more branches?',
renderListItems: branches => {
renderPageOfBranches(branches, paginatedQueryOptions);
},
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
},
});
}
@@ -128,7 +124,7 @@ function renderPageOfBranches(
export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<UpdateBranchBasicInfoFragment> {
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync(
graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
@@ -137,17 +133,15 @@ export async function createUpdateBranchOnAppAsync(
updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) {
id
...UpdateBranchBasicInfoFragment
name
}
}
}
${print(UpdateBranchBasicInfoFragmentNode)}
`,
{
appId,
name,
},
{ additionalTypenames: ['UpdateBranch'] }
}
)
.toPromise()
);
@@ -167,21 +161,22 @@ export async function ensureBranchExistsAsync(
appId: string;
branchName: string;
}
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> {
): Promise<{ branchId: string; createdBranch: boolean }> {
try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: branchName,
});
return { branch: updateBranch, createdBranch: false };
const { id } = updateBranch;
return { branchId: id, createdBranch: false };
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: branchName,
});
return { branch: newUpdateBranch, createdBranch: true };
return { branchId: newUpdateBranch.id, createdBranch: true };
} else {
throw error;
}
+6 -3
View File
@@ -1,7 +1,10 @@
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> {
return await vcsClient.getBranchNameAsync();
export async function getDefaultBranchNameAsync(): Promise<string> {
return (
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substring(2, 4)}`
);
}
export class BranchNotFoundError extends Error {
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated';
import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts';
@@ -47,14 +53,14 @@ describe(selectBuildToCancelAsync.name, () => {
it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
await expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
expect(selectBuildToCancelAsync(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);
await expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId
);
});
@@ -85,6 +91,6 @@ function createMockBuildFragment({
},
},
status: BuildStatus.InQueue,
isForIosSimulator: false,
resourceClass: BuildResourceClass.AndroidMedium,
};
}
@@ -1,77 +0,0 @@
import { AppVersionSource, EasJsonAccessor } from '@expo/eas-json';
import fs from 'fs-extra';
import { vol } from 'memfs';
import { easCliVersion } from '../../utils/easCli';
import GitClient from '../../vcs/clients/git';
import { Client } from '../../vcs/vcs';
import { ensureProjectConfiguredAsync } from '../configure';
jest.mock('fs');
jest.mock('../../vcs/vcs');
jest.mock('../../vcs/clients/git');
beforeEach(async () => {
vol.reset();
});
describe(ensureProjectConfiguredAsync, () => {
it('returns false and does not configure if eas.json exists', async () => {
vol.fromJSON({
'./eas.json': JSON.stringify({
cli: {
version: `>= ${easCliVersion}`,
},
build: {
development: {
developmentClient: true,
distribution: 'internal',
},
preview: {
distribution: 'internal',
},
production: {},
},
submit: {
production: {},
},
}),
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeFalsy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
});
it('returns true and configures if eas.json does not exist', async () => {
const writeFileMock = jest.spyOn(fs, 'writeFile');
writeFileMock.mockImplementation(async (...args) => {
const easJsonPath = args[0] as string;
const easJsonData = args[1] as string;
vol.fromJSON({
[easJsonPath]: easJsonData,
});
});
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeFalsy();
const vcsClientMock = jest.mocked(new GitClient({ requireCommit: false }));
vcsClientMock.showChangedFilesAsync.mockImplementation(async () => {});
vcsClientMock.isCommitRequiredAsync.mockImplementation(async () => false);
vcsClientMock.trackFileAsync.mockImplementation(async () => {});
const result = await ensureProjectConfiguredAsync({
projectDir: '.',
nonInteractive: false,
vcsClient: vcsClientMock as unknown as Client,
});
expect(result).toBeTruthy();
await expect(fs.pathExists(EasJsonAccessor.formatEasJsonPath('.'))).resolves.toBeTruthy();
const easJson = await EasJsonAccessor.fromProjectPath('.').readAsync();
expect(easJson.cli?.appVersionSource).toEqual(AppVersionSource.REMOTE);
});
});
@@ -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);
await 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);
await 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,
};
}
@@ -1,26 +0,0 @@
import Log from '../../../log';
import { maybeWarnAboutNonStandardBuildType } from '../build';
const warn = jest.spyOn(Log, 'warn');
describe('maybeWarnAboutNonStandardBuildType', () => {
it('should not warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildRelease',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'release' });
expect(warn).not.toHaveBeenCalled();
});
it('should warn about non-standard build type', () => {
const buildProfile = {
gradleCommand: ':app:buildStage',
};
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'stage' });
expect(warn).toHaveBeenCalled();
});
});
@@ -1,45 +1,22 @@
import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile } from '@expo/eas-json/build/build/types';
import assert from 'assert';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import path from 'path';
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { resolveVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import {
BumpStrategy,
bumpVersionAsync,
bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync,
resolveRemoteVersionCodeAsync,
} from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
jest.mock('../../../vcs/vcs');
jest.mock('../../../graphql/queries/AppVersionQuery');
jest.mock('../../../graphql/mutations/AppVersionMutation');
jest.mock('../../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
succeed: jest.fn(),
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
const vcsClient = resolveVcsClient();
afterAll(async () => {
// do not remove the following line
@@ -214,8 +191,7 @@ describe(maybeResolveVersionsAsync, () => {
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('3.0.0');
expect(appBuildVersion).toBe('123');
@@ -227,8 +203,7 @@ describe(maybeResolveVersionsAsync, () => {
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('5.0.0');
expect(appBuildVersion).toBe('126');
@@ -236,160 +211,12 @@ describe(maybeResolveVersionsAsync, () => {
});
});
describe(resolveRemoteVersionCodeAsync, () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('uses current remote versionCode when remote version set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('11');
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
});
it('initializes versionCode from local files when remote version not set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('22');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
});
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: {} as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
it('increments current remote versionCode when remote version set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('12');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('3.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
});
it('increments versionCode from local files when remote version not set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: 22, versionName: '2.3.4' });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('23');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
});
it('initializes versionCode starting with 1 when remote version not set and not set in local files, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ versionCode: null, versionName: null });
const result = await resolveRemoteVersionCodeAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationId: 'fakeApplicationId',
buildProfile: { autoIncrement: true } as AndroidBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
});
function initBareWorkflowProject({
versionCode = 123,
versionName = '3.0.0',
}: {
versionCode?: number | null;
versionName?: string | null;
versionCode?: number;
versionName?: string;
} = {}): ExpoConfig {
const fakeExp: ExpoConfig = {
name: 'myproject',
@@ -407,8 +234,8 @@ function initBareWorkflowProject({
'./android/app/build.gradle': `android {
defaultConfig {
applicationId "com.expo.testapp"
${versionCode ? `versionCode "${versionCode}"` : ''}
${versionName ? `versionName "${versionName}"` : ''}
versionCode ${versionCode}
versionName "${versionName}"
}
}`,
'./android/app/src/main/AndroidManifest.xml': 'fake',
+13 -65
View File
@@ -1,19 +1,14 @@
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 chalk from 'chalk';
import getenv from 'getenv';
import nullthrows from 'nullthrows';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
import AndroidCredentialsProvider, {
AndroidCredentials,
} from '../../credentials/android/AndroidCredentialsProvider';
import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import {
ensureApplicationIdIsDefinedForManagedProjectAsync,
getApplicationIdAsync,
@@ -34,6 +29,10 @@ import {
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteVersionCodeAsync } from './version';
export async function createAndroidContextAsync(
ctx: CommonContext<Platform.ANDROID>
@@ -60,26 +59,13 @@ This means that it will most likely produce an AAB and you will not be able to i
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(
ctx.projectDir,
buildProfile,
ctx.vcsClient
);
if (gradleContext?.buildType) {
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: gradleContext.buildType });
}
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
const applicationId = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
gradleContext
);
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
const versionCodeOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
@@ -88,7 +74,6 @@ This means that it will most likely produce an AAB and you will not be able to i
exp: ctx.exp,
applicationId,
buildProfile,
vcsClient: ctx.vcsClient,
})
: undefined;
@@ -104,21 +89,20 @@ export async function prepareAndroidBuildAsync(
return await ensureAndroidCredentialsAsync(ctx);
},
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
exp: ctx.exp,
localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.env,
projectId: ctx.projectId,
});
},
prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials>
): Promise<Android.Job> => {
): Promise<Job> => {
return await prepareJobAsync(ctx, jobData);
},
sendBuildRequestAsync: async (
@@ -140,7 +124,8 @@ export async function prepareAndroidBuildAsync(
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
const isCustomBuild = ctx.buildProfile.config !== undefined;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
}
async function ensureAndroidCredentialsAsync(
@@ -152,11 +137,9 @@ async function ensureAndroidCredentialsAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
name: ctx.buildProfile.keystoreName,
app: {
account: nullthrows(
ctx.user.accounts.find(a => a.name === ctx.accountName),
@@ -173,38 +156,3 @@ async function ensureAndroidCredentialsAsync(
source: credentialsSource,
};
}
export function maybeWarnAboutNonStandardBuildType({
buildProfile,
buildType,
}: {
buildProfile: Pick<CommonContext<Platform.ANDROID>['buildProfile'], 'gradleCommand'>;
buildType: string;
}): void {
const shouldSuppressWarning = getenv.boolish('EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING', false);
if (shouldSuppressWarning) {
return;
}
const knownValidBuildTypes = ['debug', 'release'];
if (!knownValidBuildTypes.includes(buildType)) {
Log.warn(
`Custom gradle command "${chalk.bold(
buildProfile.gradleCommand
)}" has non-standard build type: "${chalk.bold(
buildType
)}". Expected the command to end with "${chalk.bold('...Debug')}" or "${chalk.bold(
'...Release'
)}"`
);
Log.warn(
`Ensure the command is spelled correctly and ${chalk.bold(
'build.gradle'
)} is configured correctly. To suppress this warning, set ${chalk.bold(
'EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING=true'
)}.`
);
Log.warn(learnMore('https://developer.android.com/build/build-variants#build-types'));
}
}
@@ -2,13 +2,11 @@ import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput {
return {
@@ -16,6 +14,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets,
builderEnvironment: job.builderEnvironment,
@@ -30,10 +29,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
};
}
@@ -3,16 +3,18 @@ import {
ArchiveSource,
BuildMode,
BuildTrigger,
Job,
Platform,
sanitizeBuildJob,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import path from 'path';
import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -28,11 +30,11 @@ const cacheDefaults = {
export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>,
jobData: JobData
): Promise<Android.Job> {
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -53,16 +55,9 @@ export async function prepareJobAsync(
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPathForJob(buildProfile.config)
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
let buildMode;
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
}
const job: Android.Job = {
type: ctx.workflow,
platform: Platform.ANDROID,
@@ -71,15 +66,10 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
@@ -89,12 +79,12 @@ export async function prepareJobAsync(
secrets: {
...buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
buildType,
username,
...(ctx.android.versionCodeOverride && {
@@ -105,19 +95,14 @@ export async function prepareJobAsync(
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildMode,
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
appId: ctx.projectId,
initiatingUserId: ctx.user.id,
};
return sanitizeBuildJob(job) as Android.Job;
return sanitizeJob(job);
}
@@ -1,38 +1,39 @@
import { ExpoConfig } from '@expo/config';
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 chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { Client } from '../../vcs/vcs';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
projectDir,
exp,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
exp,
localAutoIncrement,
projectId,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else {
+10 -30
View File
@@ -18,7 +18,6 @@ import {
} from '../../project/android/gradleUtils';
import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
@@ -44,7 +43,7 @@ export async function bumpVersionAsync({
const buildGradle = await getAppBuildGradleAsync(projectDir);
const isMultiFlavor =
buildGradle.android?.productFlavors ?? buildGradle.android?.flavorDimensions;
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
if (isMultiFlavor) {
throw new Error(
'Automatic version bumping is not supported for multi-flavor Android projects.'
@@ -97,10 +96,9 @@ export async function bumpVersionInAppJsonAsync({
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client
): Promise<{ appVersion?: string; appBuildVersion?: string; isVersionInitialized?: boolean }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir);
try {
@@ -113,11 +111,6 @@ export async function maybeResolveVersionsAsync(
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
isVersionInitialized: !resolveConfigValue(
buildGradle,
'versionCode',
parsedGradleCommand?.flavor
),
};
} catch {
return {};
@@ -190,14 +183,12 @@ export async function resolveRemoteVersionCodeAsync(
exp,
applicationId,
buildProfile,
vcsClient,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationId: string;
buildProfile: BuildProfile<Platform.ANDROID>;
vcsClient: Client;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
@@ -207,20 +198,16 @@ export async function resolveRemoteVersionCodeAsync(
applicationId
);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
let currentBuildVersion: string;
let shouldInitializeVersionCode = false;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localVersions.appBuildVersion) {
Log.log(
chalk.green(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
)
Log.warn(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
);
currentBuildVersion = localVersions.appBuildVersion;
shouldInitializeVersionCode = localVersions.isVersionInitialized ?? false;
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
@@ -230,10 +217,7 @@ export async function resolveRemoteVersionCodeAsync(
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeVersionCode
) {
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start();
@@ -244,9 +228,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
@@ -268,9 +250,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion),
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
});
spinner.succeed(
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
+161 -238
View File
@@ -1,12 +1,5 @@
import {
ArchiveSource,
ArchiveSourceType,
BuildJob,
FingerprintSource,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
@@ -14,6 +7,36 @@ import fs from 'fs-extra';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import {
AppPlatform,
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildResourceClass,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora';
import {
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { confirmAsync } from '../prompts';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import {
EasBuildDownForMaintenanceError,
@@ -22,7 +45,6 @@ import {
EasBuildFreeTierDisabledIOSError,
EasBuildFreeTierIosLimitExceededError,
EasBuildFreeTierLimitExceededError,
EasBuildLegacyResourceClassNotAvailableError,
EasBuildProjectArchiveUploadError,
EasBuildResourceClassNotAvailableInFreeTierError,
EasBuildTooManyPendingBuildsError,
@@ -33,45 +55,7 @@ import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo';
import {
LocalFile,
assertProjectTarballSizeDoesNotExceedLimit,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
maybeWarnAboutProjectTarballSize,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors';
import { createFingerprintAsync } from '../fingerprint/cli';
import {
AppPlatform,
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora';
import {
RequestedPlatform,
appPlatformDisplayNames,
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
export interface CredentialsResult<Credentials> {
source: CredentialsSource.LOCAL | CredentialsSource.REMOTE;
@@ -83,14 +67,14 @@ export interface JobData<Credentials> {
projectArchive: ArchiveSource;
}
interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob> {
interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>;
ensureCredentialsAsync(
ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>;
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(
appId: string,
job: TJob,
@@ -115,7 +99,7 @@ function resolveBuildParamsInput<T extends Platform>(
export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform,
Credentials,
TJob extends BuildJob,
TJob extends Job
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync(
@@ -131,9 +115,7 @@ export async function prepareBuildRequestForPlatformAsync<
await withAnalyticsAsync(
ctx.analytics,
async () => {
await builder.syncProjectConfigurationAsync(ctx);
},
async () => await builder.syncProjectConfigurationAsync(ctx),
{
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
@@ -142,27 +124,19 @@ export async function prepareBuildRequestForPlatformAsync<
}
);
if (await ctx.vcsClient.isCommitRequiredAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
const platformToRequestedPlatform: Record<Platform, RequestedPlatform> = {
[Platform.ANDROID]: RequestedPlatform.Android,
[Platform.IOS]: RequestedPlatform.Ios,
};
await reviewAndCommitChangesAsync(
ctx.vcsClient,
`[EAS Build] Run EAS Build for ${
requestedPlatformDisplayNames[platformToRequestedPlatform[ctx.platform]]
}`,
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
}
let projectArchive: ArchiveSource | undefined;
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
const projectPath = (await makeProjectTarballAsync(ctx.vcsClient)).path;
projectArchive = {
type: ArchiveSourceType.PATH,
path: projectPath,
path: (await makeProjectTarballAsync()).path,
};
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = {
@@ -172,14 +146,12 @@ export async function prepareBuildRequestForPlatformAsync<
} else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = {
type: ArchiveSourceType.GCS,
...(await uploadProjectAsync(ctx)),
bucketKey: await uploadProjectAsync(ctx),
};
}
assert(projectArchive);
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync(ctx);
const metadata = await collectMetadataAsync(ctx, runtimeAndFingerprintMetadata);
const metadata = await collectMetadataAsync(ctx);
const buildParams = resolveBuildParamsInput(ctx, metadata);
const job = await builder.prepareJobAsync(ctx, {
projectArchive,
@@ -188,7 +160,7 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env);
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
@@ -218,7 +190,6 @@ const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED: EasBuildFreeTierIosLimitExceededError,
EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER:
EasBuildResourceClassNotAvailableInFreeTierError,
EAS_BUILD_LEGACY_RESOURCE_CLASS_NOT_AVAILABLE: EasBuildLegacyResourceClassNotAvailableError,
VALIDATION_ERROR: RequestValidationError,
};
@@ -260,10 +231,7 @@ export function handleBuildRequestError(error: any, platform: Platform): never {
async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform>
): Promise<{
bucketKey: string;
metadataLocation?: string;
}> {
): Promise<string> {
let projectTarballPath;
try {
return await withAnalyticsAsync(
@@ -275,33 +243,38 @@ async function uploadProjectAsync<TPlatform extends Platform>(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
const projectTarball = await makeProjectTarballAsync();
maybeWarnAboutProjectTarballSize(projectTarball.size);
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
`Your project archive is ${formatBytes(
projectTarball.size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
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) {
return { bucketKey, metadataLocation };
}
return { bucketKey };
const bucketKey = await 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)}`,
})
);
return bucketKey;
},
{
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
@@ -312,11 +285,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
);
} 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 {
if (projectTarballPath) {
@@ -325,41 +296,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
}
}
async function uploadMetadataFileAsync<TPlatform extends Platform>(
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,
>(
async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
builder: Builder<TPlatform, Credentials, TJob>,
job: TJob,
metadata: Metadata,
@@ -396,7 +333,12 @@ export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
buildIds,
accountName,
projectDir,
nonInteractive,
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
@@ -413,7 +355,10 @@ export async function waitForBuildEndAsync(
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
? await handleSingleBuildProgressAsync(
{ build: builds[0], accountName, projectDir, nonInteractive },
{ spinner }
)
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
@@ -451,9 +396,13 @@ async function handleSingleBuildProgressAsync(
{
build,
accountName,
projectDir,
nonInteractive,
}: {
build: MaybeBuildFragment;
accountName: string;
projectDir: string;
nonInteractive: boolean;
},
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
@@ -479,8 +428,8 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished');
return { refetch: false };
case BuildStatus.New:
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
formatAccountBillingUrl(accountName)
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountSubscriptionsUrl(accountName)
)}.`;
break;
case BuildStatus.InQueue: {
@@ -500,7 +449,59 @@ async function handleSingleBuildProgressAsync(
if (build.priority !== BuildPriority.High) {
Log.newLine();
Log.log('Start builds sooner in the priority queue.');
Log.log(`Sign up for a paid plan at ${link(formatAccountBillingUrl(accountName))}`);
Log.log(
`Sign up for EAS Production or Enterprise at ${link(
formatAccountSubscriptionsUrl(accountName)
)}`
);
}
if (
build.platform === AppPlatform.Ios &&
[BuildResourceClass.IosIntelLarge, BuildResourceClass.IosIntelMedium].includes(
build.resourceClass
)
) {
let askToSwitchToM1 = false;
if (
build.priority === BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 10 minutes */ 10 * 60
) {
Log.newLine();
Log.warn(
`Warning: Priority queue wait time for legacy iOS Intel workers is longer than usual (more than 10 minutes).`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
} else if (
build.priority !== BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 120 minutes */ 120 * 60
) {
Log.newLine();
Log.warn(
`Warning: Free tier queue wait time for legacy iOS Intel workers is longer than usual.`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
}
if (!nonInteractive && askToSwitchToM1) {
const shouldSwitchToM1 = await confirmAsync({
message: `Switch iOS builds to M1 workers (modifies build profiles in eas.json)?`,
});
if (shouldSwitchToM1) {
await updateIosBuildProfilesToUseM1WorkersAsync(projectDir);
}
}
}
Log.newLine();
@@ -645,105 +646,27 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
}
}
function formatAccountBillingUrl(accountName: string): string {
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString();
function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(
`/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
).toString();
}
async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}> {
const runtimeAndFingerprintMetadata =
await computeAndMaybeUploadFingerprintFromExpoUpdatesAsync(ctx);
if (!runtimeAndFingerprintMetadata?.fingerprintHash) {
const fingerprint = await computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync(ctx);
return {
...runtimeAndFingerprintMetadata,
...fingerprint,
};
} else {
return {
...runtimeAndFingerprintMetadata,
fingerprintHash: runtimeAndFingerprintMetadata.runtimeVersion,
};
}
}
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
async function computeAndMaybeUploadFingerprintFromExpoUpdatesAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<{
runtimeVersion?: string;
fingerprintSource?: FingerprintSource;
fingerprintHash?: string;
}> {
const resolvedRuntimeVersion = await resolveRuntimeVersionAsync({
exp: ctx.exp,
platform: ctx.platform,
workflow: ctx.workflow,
projectDir: ctx.projectDir,
env: ctx.env,
cwd: ctx.projectDir,
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
easJsonAccessor.patch(easJsonRawObject => {
for (const profileName of profileNames) {
easJsonRawObject.build[profileName].ios = {
...easJsonRawObject.build[profileName].ios,
resourceClass: ResourceClass.M_MEDIUM,
};
}
return easJsonRawObject;
});
if (!resolvedRuntimeVersion) {
return {};
}
/**
* It's ok for fingerprintSources or runtimeVersion to be empty
* fingerprintSources only exist if the project is using runtimeVersion.policy: fingerprint
*/
if (
!resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint ||
!resolvedRuntimeVersion.runtimeVersion
) {
return {
runtimeVersion: resolvedRuntimeVersion.runtimeVersion ?? undefined,
};
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: resolvedRuntimeVersion.runtimeVersion,
fingerprint: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprint,
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
return {
runtimeVersion: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
fingerprintHash: resolvedRuntimeVersion.expoUpdatesRuntimeFingerprintHash ?? undefined,
};
}
async function computeAndMaybeUploadFingerprintWithoutExpoUpdatesAsync<T extends Platform>(
ctx: BuildContext<T>,
{ debug }: { debug?: boolean } = {}
): Promise<{
fingerprintHash?: string;
fingerprintSource?: FingerprintSource;
}> {
const fingerprint = await createFingerprintAsync(ctx.projectDir, {
workflow: ctx.workflow,
platforms: [ctx.platform],
env: ctx.env,
});
if (!fingerprint) {
return {};
}
const uploadedFingerprint = await maybeUploadFingerprintAsync({
hash: fingerprint.hash,
fingerprint: {
fingerprintSources: fingerprint.sources,
isDebugFingerprintSource: debug ?? false,
},
graphqlClient: ctx.graphqlClient,
localBuildMode: ctx.localBuildOptions.localBuildMode,
});
return {
fingerprintHash: uploadedFingerprint.hash,
fingerprintSource: uploadedFingerprint.fingerprintSource,
};
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
}
+48 -94
View File
@@ -1,17 +1,17 @@
import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json';
import { Platform, Workflow } from '@expo/eas-build-job';
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk';
import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
interface ConfigureParams {
projectDir: string;
nonInteractive: boolean;
vcsClient: Client;
}
export async function easJsonExistsAsync(projectDir: string): Promise<boolean> {
@@ -36,99 +36,22 @@ export async function ensureProjectConfiguredAsync(
return true;
}
async function configureAsync({
projectDir,
nonInteractive,
vcsClient,
}: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync(vcsClient, nonInteractive);
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync();
await createEasJsonAsync(projectDir, vcsClient);
await createEasJsonAsync(projectDir);
if (await vcsClient.isCommitRequiredAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(vcsClient, 'Configure EAS Build', {
await reviewAndCommitChangesAsync('Configure EAS Build', {
nonInteractive,
});
}
}
export async function doesBuildProfileExistAsync({
projectDir,
profileName,
}: {
projectDir: string;
profileName: string;
}): Promise<boolean> {
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJson = await easJsonAccessor.readRawJsonAsync();
if (!easJson.build?.[profileName]) {
return false;
}
return true;
} catch (error) {
Log.error(`We were unable to read ${chalk.bold('eas.json')} contents. Error: ${error}.`);
throw error;
}
}
export async function createBuildProfileAsync({
projectDir,
profileName,
profileContents,
vcsClient,
nonInteractive,
}: {
projectDir: string;
profileName: string;
profileContents: Record<string, any>;
vcsClient: Client;
nonInteractive: boolean;
}): Promise<void> {
const spinner = ora(`Adding "${profileName}" build profile to ${chalk.bold('eas.json')}`).start();
try {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
easJsonAccessor.patch(easJsonRawObject => {
return {
...easJsonRawObject,
build: {
...easJsonRawObject.build,
[profileName]: profileContents,
},
};
});
await easJsonAccessor.writeAsync();
spinner.succeed(
`Successfully added "${profileName}" build profile to ${chalk.bold('eas.json')}.`
);
if (await vcsClient.isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(
vcsClient,
`Add "${profileName}" build profile to eas.json`,
{
nonInteractive,
}
);
}
} catch (error) {
spinner.fail(
`We were not able to configure "${profileName}" build profile inside of ${chalk.bold(
'eas.json'
)}. Error: ${error}.`
);
throw error;
}
}
const EAS_JSON_DEFAULT: EasJson = {
const EAS_JSON_MANAGED_DEFAULT: EasJson = {
cli: {
version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
},
build: {
development: {
@@ -138,20 +61,51 @@ const EAS_JSON_DEFAULT: EasJson = {
preview: {
distribution: 'internal',
},
production: {
autoIncrement: true,
},
production: {},
},
submit: {
production: {},
},
};
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);
await fs.writeFile(easJsonPath, `${JSON.stringify(EAS_JSON_DEFAULT, null, 2)}\n`);
await vcsClient.trackFileAsync(easJsonPath);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
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')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
+1 -11
View File
@@ -1,10 +1,7 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { NodePackageManager } from '@expo/package-manager';
import { LocalBuildOptions } from './local';
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { CredentialsContext } from '../credentials/context';
@@ -14,7 +11,7 @@ import { GradleBuildContext } from '../project/android/gradle';
import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
import { XcodeBuildContext } from '../project/ios/scheme';
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'>;
@@ -58,11 +55,4 @@ export interface BuildContext<T extends Platform> {
customBuildConfigMetadata?: CustomBuildConfigMetadata;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
developmentClient: boolean;
requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client;
loggerLevel?: LoggerLevel;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}
+8 -43
View File
@@ -1,17 +1,10 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import { LoggerLevel } from '@expo/logger';
import { resolvePackageManager } from '@expo/package-manager';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildMode, 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';
@@ -20,7 +13,11 @@ 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';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
import { LocalBuildOptions } from './local';
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
export async function createBuildContextAsync<T extends Platform>({
buildProfileName,
@@ -37,14 +34,8 @@ export async function createBuildContextAsync<T extends Platform>({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
isVerboseLoggingEnabled,
whatToTest,
env,
}: {
buildProfileName: string;
buildProfile: BuildProfile<T>;
@@ -60,31 +51,15 @@ export async function createBuildContextAsync<T extends Platform>({
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
isVerboseLoggingEnabled: boolean;
whatToTest?: string;
env: Record<string, string>;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
env,
});
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = account.id;
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({
projectInfo: { exp, projectId },
@@ -93,10 +68,8 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
env,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
freezeCredentials,
});
const devClientProperties = getDevClientEventProperties({
@@ -114,7 +87,6 @@ export async function createBuildContextAsync<T extends Platform>({
...devClientProperties,
no_wait: noWait,
run_from_ci: runFromCI,
local: localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN,
};
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
@@ -144,17 +116,10 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
vcsClient,
workflow,
message,
runFromCI,
customBuildConfigMetadata,
developmentClient,
requiredPackageManager,
loggerLevel: buildLoggerLevel,
isVerboseLoggingEnabled,
env,
whatToTest,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
-2
View File
@@ -14,8 +14,6 @@ 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 {}
@@ -1,147 +0,0 @@
import { Env } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { isEnvironment } from './utils/environment';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
import Log, { learnMore } from '../log';
export async function evaluateConfigWithEnvVarsAsync<Config extends { projectId: string }, Opts>({
buildProfile,
buildProfileName,
graphqlClient,
getProjectConfig,
opts,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient | null;
opts: Opts;
getProjectConfig(opts: Opts): Promise<Config>;
}): Promise<Config & { env: Env }> {
if (!graphqlClient) {
Log.warn('An Expo user account is required to fetch environment variables.');
const config = await getProjectConfig(opts);
return { env: buildProfile.env ?? {}, ...config };
}
const { projectId } = await getProjectConfig({ env: buildProfile.env, ...opts });
const env = await resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
});
const config = await getProjectConfig({ ...opts, env });
return { env, ...config };
}
async function resolveEnvVarsAsync({
buildProfile,
buildProfileName,
graphqlClient,
projectId,
}: {
buildProfile: BuildProfile;
buildProfileName: string;
graphqlClient: ExpoGraphqlClient;
projectId: string;
}): Promise<Env> {
const environment =
buildProfile.environment?.toUpperCase() ??
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
if (!isEnvironment(environment)) {
Log.log(
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
buildProfile.env && Object.keys(buildProfile.env).length > 0
? Object.keys(buildProfile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
return { ...buildProfile.env };
}
try {
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
graphqlClient,
{
appId: projectId,
environment,
}
);
const serverEnvVars = Object.fromEntries(
environmentVariables
.filter(({ name, value }) => name && value)
.map(({ name, value }) => [name, value])
) as Record<string, string>;
if (Object.keys(serverEnvVars).length > 0) {
Log.log(
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
serverEnvVars
).join(', ')}.`
);
} else {
Log.log(
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
);
}
if (buildProfile.env && Object.keys(buildProfile.env).length > 0) {
Log.log(
`Environment variables loaded from the "${buildProfileName}" build profile "env" configuration: ${
buildProfile.env && Object.keys(buildProfile.env).join(', ')
}.`
);
}
if (
buildProfile.env &&
Object.keys(buildProfile.env).length > 0 &&
Object.keys(serverEnvVars).length > 0
) {
const overlappingKeys = Object.keys(serverEnvVars).filter(
key => buildProfile.env && Object.keys(buildProfile.env).includes(key)
);
if (overlappingKeys.length > 0) {
Log.warn(
`The following environment variables are defined in both the "${buildProfileName}" build profile "env" configuration and the "${environment.toLowerCase()}" environment on EAS: ${overlappingKeys.join(
', '
)}. The values from the build profile configuration will be used.`
);
}
}
Log.newLine();
return { ...serverEnvVars, ...buildProfile.env };
} catch (e) {
Log.error(`Failed to pull env variables for environment ${environment} from EAS servers`);
Log.error(e);
Log.error(
'This can possibly be a bug in EAS/EAS CLI. Report it here: https://github.com/expo/eas-cli/issues'
);
return { ...buildProfile.env };
}
}
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
buildProfile: BuildProfile
): EnvironmentVariableEnvironment {
const environment =
buildProfile.distribution === 'store'
? EnvironmentVariableEnvironment.Production
: buildProfile.developmentClient
? EnvironmentVariableEnvironment.Development
: EnvironmentVariableEnvironment.Preview;
Log.log(
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
)}`
);
return environment;
}
+19 -46
View File
@@ -1,36 +1,33 @@
import {
ArchiveSource,
ArchiveSourceType,
BuildMode,
BuildTrigger,
FingerprintSourceType,
Metadata,
Workflow,
} from '@expo/eas-build-job';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import {
BuildCredentialsSource,
BuildIosEnterpriseProvisioning,
BuildMetadataInput,
BuildMode,
BuildWorkflow,
DistributionType,
FingerprintSourceInput,
BuildMode as GraphQLBuildMode,
BuildTrigger as GraphQLBuildTrigger,
FingerprintSourceType as GraphQLFingeprintSourceType,
ProjectArchiveSourceInput,
ProjectArchiveSourceType,
WorkerLoggerLevel,
} from '../graphql/generated';
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
if (archiveSource.type === ArchiveSourceType.GCS) {
if (archiveSource.type === ArchiveSourceType.S3) {
return {
type: ProjectArchiveSourceType.S3,
bucketKey: archiveSource.bucketKey,
};
} else if (archiveSource.type === ArchiveSourceType.GCS) {
return {
type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey,
metadataLocation: archiveSource.metadataLocation,
};
} else if (archiveSource.type === ArchiveSourceType.URL) {
return {
@@ -45,8 +42,7 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return {
...metadata,
fingerprintSource:
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
buildMode: metadata.buildMode && transformBuildMode(metadata.buildMode),
credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -57,20 +53,6 @@ export function transformMetadata(metadata: Metadata): BuildMetadataInput {
};
}
export function transformFingerprintSource(
fingerprintSource: NonNullable<Metadata['fingerprintSource']>
): FingerprintSourceInput | null {
if (fingerprintSource.type !== FingerprintSourceType.GCS) {
return null;
}
return {
type: GraphQLFingeprintSourceType.Gcs,
bucketKey: fingerprintSource.bucketKey,
isDebugFingerprint: fingerprintSource.isDebugFingerprint,
};
}
function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource {
@@ -109,17 +91,17 @@ export function transformIosEnterpriseProvisioning(
}
}
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;
// TODO: check what in metadata
export function transformBuildMode(buildMode: string): BuildMode {
if (buildMode === 'build') {
return BuildMode.Build;
} else if (buildMode === 'resign') {
return BuildMode.Resign;
} else if (buildMode === 'custom') {
return BuildMode.Custom;
} else {
throw new Error(`Unsupported build mode: ${buildMode}`);
}
}
export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildTrigger {
@@ -130,12 +112,3 @@ export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildT
}
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,
};
@@ -1,19 +1,11 @@
import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { IosBuildProfile } from '@expo/eas-json/build/build/types';
import fs from 'fs-extra';
import { vol } from 'memfs';
import os from 'os';
import { instance, mock } from 'ts-mockito';
import type { XCBuildConfiguration } from 'xcode';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../../credentials/ios/types';
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
import { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -22,25 +14,9 @@ import {
getInfoPlistPath,
readBuildNumberAsync,
readShortVersionAsync,
resolveRemoteBuildNumberAsync,
} from '../version';
jest.mock('fs');
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
jest.mock('../../../vcs/vcs');
jest.mock('../../../graphql/queries/AppVersionQuery');
jest.mock('../../../graphql/mutations/AppVersionMutation');
jest.mock('../../../ora', () => ({
ora: jest.fn().mockImplementation(() => ({
start: jest.fn().mockImplementation(() => ({
succeed: jest.fn(),
stop: jest.fn(),
fail: jest.fn(),
})),
})),
}));
const vcsClient = resolveVcsClient();
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target,
@@ -83,7 +59,7 @@ describe(evaluateTemplateString, () => {
// bare workflow
describe(bumpVersionAsync, () => {
beforeEach(() => {
getPbxproj.mockImplementation((): any => {});
getPbxproj.mockImplementation(() => ({} as any));
});
afterEach(() => {
getXCBuildConfigurationFromPbxproj.mockReset();
@@ -97,7 +73,7 @@ describe(bumpVersionAsync, () => {
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
}) as XCBuildConfiguration
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -133,7 +109,7 @@ describe(bumpVersionAsync, () => {
buildSettings: {
INFOPLIST_FILE: '$(SRCROOT)/myproject/Info2.plist',
},
}) as XCBuildConfiguration
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.BUILD_NUMBER,
@@ -168,7 +144,7 @@ describe(bumpVersionAsync, () => {
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
}) as XCBuildConfiguration
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.APP_VERSION,
@@ -203,7 +179,7 @@ describe(bumpVersionAsync, () => {
buildSettings: {
INFOPLIST_FILE: 'myproject/Info.plist',
},
}) as XCBuildConfiguration
} as XCBuildConfiguration)
);
await bumpVersionAsync({
bumpStrategy: BumpStrategy.NOOP,
@@ -286,7 +262,7 @@ describe(readBuildNumberAsync, () => {
describe('bare project', () => {
it('reads the build number from native code', async () => {
const exp = initBareWorkflowProject();
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
const buildNumber = await readBuildNumberAsync('/app', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -294,7 +270,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
const buildNumber = await readBuildNumberAsync('/app', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -304,29 +280,38 @@ describe(readShortVersionAsync, () => {
describe('bare project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
const appVersion = await readShortVersionAsync('/app', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync(
'/app',
exp,
{
CURRENT_PROJECT_VERSION: '1.0.0',
},
vcsClient
);
const buildNumber = await readShortVersionAsync('/app', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
it('fails when build number is invalid', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
await expect(
readShortVersionAsync('/app', exp, {
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
})
).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', () => {
it('reads the version from app config', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
const appVersion = await readShortVersionAsync('/app', exp, {});
expect(appVersion).toBe('1.0.0');
});
});
@@ -380,154 +365,6 @@ describe(getInfoPlistPath, () => {
});
});
describe(resolveRemoteBuildNumberAsync, () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('uses current remote buildNumber when remote version set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('11');
expect(createAppVersionAsyncSpy).not.toHaveBeenCalled();
});
it('initializes buildNumber from local files when remote version not set, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('22');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('22');
});
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=false', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: {} as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
it('increments current remote buildNumber when remote version set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue({
buildVersion: '11',
storeVersion: '1.2.3',
});
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject();
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('12');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('12');
});
it('increments buildNumber from local files when remote version not set, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '22', appVersion: '2.3.4' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('23');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('2.3.4');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('23');
});
it('initializes buildNumber starting with 1 when remote version not set and local version is 1, autoIncrement=true', async () => {
const graphQLClientMock = instance(mock<ExpoGraphqlClient>());
const vcsClientMock = instance(mock<Client>());
vcsClientMock.getRootPathAsync = async () => '/app';
jest.mocked(AppVersionQuery.latestVersionAsync).mockResolvedValue(null);
const createAppVersionAsyncSpy = jest.spyOn(AppVersionMutation, 'createAppVersionAsync');
const exp = initBareWorkflowProject({ buildNumber: '1', appVersion: '1.0.0' });
const result = await resolveRemoteBuildNumberAsync(graphQLClientMock, {
projectDir: '/app',
projectId: 'fakeProjectId',
exp,
applicationTarget: {} as Target,
buildProfile: { autoIncrement: true } as IosBuildProfile,
vcsClient: vcsClientMock,
});
expect(result).toBe('1');
expect(createAppVersionAsyncSpy).toHaveBeenCalled();
expect(createAppVersionAsyncSpy.mock.calls[0][1].storeVersion).toBe('1.0.0');
expect(createAppVersionAsyncSpy.mock.calls[0][1].buildVersion).toBe('1');
});
});
function initBareWorkflowProject({
appVersion = '1.0.0',
buildNumber = '1',
+12 -18
View File
@@ -1,16 +1,10 @@
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 { 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 { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
@@ -21,15 +15,19 @@ import {
checkNodeEnvVariable,
validatePNGsForManagedProjectAsync,
} from '../validate';
import { ensureIosCredentialsAsync } from './credentials';
import { transformJob } from './graphql';
import { prepareJobAsync } from './prepareJob';
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
import { resolveRemoteBuildNumberAsync } from './version';
export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> {
const { buildProfile, env } = ctx;
const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
}
checkNodeEnvVariable(ctx);
@@ -41,7 +39,6 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
vcsClient: ctx.vcsClient,
},
buildProfile
);
@@ -49,8 +46,7 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
exp: ctx.exp,
xcodeBuildContext,
env,
vcsClient: ctx.vcsClient,
env: buildProfile.env,
});
const applicationTarget = findApplicationTarget(targets);
const buildNumberOverride =
@@ -61,7 +57,6 @@ export async function createIosContextAsync(
exp: ctx.exp,
applicationTarget,
buildProfile,
vcsClient: ctx.vcsClient,
})
: undefined;
return {
@@ -79,10 +74,10 @@ export async function prepareIosBuildAsync(
return await prepareBuildRequestForPlatformAsync({
ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return await ensureIosCredentialsAsync(ctx, ctx.ios.targets);
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
},
syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({
await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir,
exp: ctx.exp,
targets: ctx.ios.targets,
@@ -90,14 +85,13 @@ export async function prepareIosBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false
: ctx.buildProfile.autoIncrement,
vcsClient: ctx.vcsClient,
env: ctx.env,
projectId: ctx.projectId,
});
},
prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials>
): Promise<Ios.Job> => {
): Promise<Job> => {
return await prepareJobAsync(ctx, {
...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
@@ -55,5 +55,6 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator && !buildCtx.buildProfile.withoutCredentials;
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
}
+1 -6
View File
@@ -3,13 +3,11 @@ import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode,
transformBuildTrigger,
transformProjectArchive,
transformWorkflow,
} from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput {
return {
@@ -17,6 +15,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment,
@@ -32,10 +31,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental,
mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
};
}
+14 -29
View File
@@ -3,8 +3,9 @@ import {
BuildMode,
BuildTrigger,
Ios,
Job,
Platform,
sanitizeBuildJob,
sanitizeJob,
} from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows';
@@ -13,8 +14,9 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -31,11 +33,11 @@ const cacheDefaults = {
export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>,
jobData: JobData
): Promise<Ios.Job> {
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -45,16 +47,9 @@ export async function prepareJobAsync(
}
const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPathForJob(buildProfile.config)
? getCustomBuildConfigPath(buildProfile.config)
: undefined;
let buildMode;
if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
}
const job: Ios.Job = {
type: ctx.workflow,
platform: Platform.IOS,
@@ -63,17 +58,12 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane,
env: {
...buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
expoCli: buildProfile.expoCli,
env: buildProfile.env,
},
cache: {
...cacheDefaults,
@@ -83,6 +73,7 @@ export async function prepareJobAsync(
secrets: {
buildCredentials,
},
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator,
@@ -90,7 +81,6 @@ export async function prepareJobAsync(
buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
username,
...(ctx.ios.buildNumberOverride && {
version: {
@@ -100,20 +90,15 @@ export async function prepareJobAsync(
experimental: {
prebuildCommand: buildProfile.prebuildCommand,
},
mode: buildMode,
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && {
customBuildConfig: {
path: maybeCustomBuildConfigPath,
},
}),
loggerLevel: ctx.loggerLevel,
// Technically, these are unused, but let's include them here for type consistency.
// See: https://github.com/expo/eas-build/pull/454
appId: ctx.projectId,
initiatingUserId: ctx.user.id,
};
return sanitizeBuildJob(job) as Ios.Job;
return sanitizeJob(job);
}
export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
@@ -1,35 +1,36 @@
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 { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { Client } from '../../vcs/vcs';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync({
projectDir,
exp,
targets,
localAutoIncrement,
vcsClient,
env,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
vcsClient: Client;
env: Env | undefined;
}): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir,
exp,
targets,
localAutoIncrement,
projectId,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
projectId: string;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env });
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
+22 -59
View File
@@ -11,7 +11,7 @@ import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
@@ -19,11 +19,10 @@ import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/;
const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
export enum BumpStrategy {
APP_VERSION,
@@ -100,37 +99,20 @@ export async function bumpVersionInAppJsonAsync({
}
}
function validateShortVersion({
shortVersion,
workflow,
}: {
shortVersion: string | undefined;
workflow: Workflow;
}): void {
function validateShortVersion(shortVersion: string | undefined): void {
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
if (workflow === Workflow.MANAGED) {
Log.warn(
`The required format for "version" field from app.json/app.config.ts for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the format required by Apple. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
} else {
Log.warn(
`The required format for "CFBundleShortVersionString" in Info.plist for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the format required by Apple. ${learnMore(
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
)}`
);
}
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(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
@@ -138,10 +120,10 @@ export async function readShortVersionAsync(
infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
validateShortVersion({ shortVersion, workflow });
validateShortVersion(shortVersion);
return shortVersion;
} else {
validateShortVersion({ shortVersion: exp.version, workflow });
validateShortVersion(exp.version);
return exp.version;
}
}
@@ -149,10 +131,9 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
@@ -166,8 +147,7 @@ export async function readBuildNumberAsync(
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
targets: Target[],
vcsClient: Client
targets: Target[]
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const applicationTarget = findApplicationTarget(targets);
try {
@@ -175,14 +155,12 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
),
appVersion: await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
),
};
} catch (err: any) {
@@ -300,14 +278,12 @@ export async function resolveRemoteBuildNumberAsync(
exp,
applicationTarget,
buildProfile,
vcsClient,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationTarget: Target;
buildProfile: BuildProfile<Platform.IOS>;
vcsClient: Client;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
@@ -320,28 +296,22 @@ export async function resolveRemoteBuildNumberAsync(
const localBuildNumber = await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
);
const localShortVersion = await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
);
let currentBuildVersion: string;
let shouldInitializeBuildNumber = false;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
} else {
if (localBuildNumber) {
Log.log(
chalk.green(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
)
Log.warn(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
);
currentBuildVersion = localBuildNumber;
shouldInitializeBuildNumber = localBuildNumber === '1';
} else {
Log.error(
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
@@ -351,10 +321,7 @@ export async function resolveRemoteBuildNumberAsync(
}
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion;
} else if (
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeBuildNumber
) {
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
const spinner = ora(
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
).start();
@@ -365,9 +332,7 @@ export async function resolveRemoteBuildNumberAsync(
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: currentBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
undefined,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) {
@@ -389,9 +354,7 @@ export async function resolveRemoteBuildNumberAsync(
applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0',
buildVersion: nextBuildVersion,
runtimeVersion:
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
undefined,
runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
});
spinner.succeed(
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
+12 -22
View File
@@ -3,11 +3,10 @@ import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process';
import semver from 'semver';
import Log from '../log';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.171';
const PLUGIN_PACKAGE_VERSION = '1.0.27';
export enum LocalBuildMode {
/**
@@ -40,8 +39,7 @@ export interface LocalBuildOptions {
export async function runLocalBuildAsync(
job: Job,
metadata: Metadata,
options: LocalBuildOptions,
env: Record<string, string>
options: LocalBuildOptions
): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner;
@@ -56,26 +54,18 @@ export async function runLocalBuildAsync(
};
process.on('SIGINT', interruptHandler);
try {
const mergedEnv = {
...env,
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
};
// log command execution to assist in debugging local builds
Log.debug('Running local build, using local-build-plugin', {
command,
args,
env: mergedEnv,
});
const spawnPromise = spawnAsync(command, args, {
stdio: options.verbose ? 'inherit' : 'pipe',
env: mergedEnv,
env: {
...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
: {}),
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
},
});
childProcess = spawnPromise.child;
await spawnPromise;
+51 -38
View File
@@ -1,32 +1,35 @@
import { FingerprintSource, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { Updates } from '@expo/config-plugins';
import { BuildMode, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra';
import resolveFrom from 'resolve-from';
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 { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log';
import {
getUsernameForBuildMetadataAndBuildJob,
isExpoUpdatesInstalled,
} from '../project/projectUtils';
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>(
ctx: BuildContext<T>,
runtimeAndFingerprintMetadata: {
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}
ctx: BuildContext<T>
): Promise<Metadata> {
const channelObject = await resolveChannelAsync(ctx);
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE;
const vcsClient = getVcsClient();
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties,
...(await maybeResolveVersionsAsync(ctx)),
@@ -34,24 +37,22 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelObject,
...channelOrReleaseChannel,
distribution,
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await ctx.vcsClient.getCommitHashAsync(),
gitCommitHash: await vcsClient.getCommitHashAsync(),
gitCommitMessage: truncateGitCommitMessage(
(await ctx.vcsClient.getLastCommitMessageAsync()) ?? undefined
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
),
isGitWorkingTreeDirty:
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
? false
: await ctx.vcsClient.hasUncommittedChangesAsync(),
username: getUsernameForBuildMetadataAndBuildJob(ctx.user),
: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
@@ -60,12 +61,8 @@ export async function collectMetadataAsync<T extends Platform>(
}),
runWithNoWaitFlag: ctx.noWait,
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
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);
}
@@ -78,8 +75,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
const resolvedVersion = await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext.ios.targets,
ctx.vcsClient
iosContext.ios.targets
);
if (iosContext.ios.buildNumberOverride) {
return {
@@ -93,8 +89,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
ctx.projectDir,
ctx.exp,
androidCtx.buildProfile,
ctx.vcsClient
androidCtx.buildProfile
);
if (androidCtx.android.versionCodeOverride) {
return {
@@ -116,21 +111,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>
): Promise<{ channel: string } | null> {
): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null;
}
if (ctx.buildProfile.channel) {
return { channel: ctx.buildProfile.channel };
}
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const channel = await getNativeChannelAsync(ctx);
if (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>(
+3 -4
View File
@@ -1,6 +1,5 @@
import chalk from 'chalk';
import { formatGraphQLBuild } from './utils/formatBuild';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { AppPlatform, BuildFilter, BuildFragment } from '../graphql/generated';
@@ -12,6 +11,7 @@ import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { formatGraphQLBuild } from './utils/formatBuild';
export const BUILDS_LIMIT = 50;
@@ -50,9 +50,8 @@ export async function listAndRenderBuildsOnAppAsync(
}),
promptOptions: {
title: 'Load more builds?',
renderListItems: builds => {
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
},
renderListItems: builds =>
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
},
});
}
+73 -261
View File
@@ -1,32 +1,18 @@
import { ExpoConfig } from '@expo/config';
import { Env, Platform, Workflow } from '@expo/eas-build-job';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
BuildProfile,
EasJson,
EasJsonAccessor,
EasJsonUtils,
ResourceClass,
SubmitProfile,
} from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert';
import chalk from 'chalk';
import { pathExists } from 'fs-extra';
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 { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
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 {
@@ -52,18 +38,17 @@ import {
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import {
installExpoUpdatesAsync,
isExpoUpdatesInstalledAsDevDependency,
isExpoUpdatesInstalledOrAvailable,
isUsingEASUpdate,
validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils';
import {
ensureAppVersionSourceIsSetAsync,
validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettingsAsync,
validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts';
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run';
import { runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context';
import {
@@ -72,13 +57,23 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
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 { LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository';
let metroConfigValidated = false;
let sdkVersionChecked = false;
@@ -95,43 +90,22 @@ export interface BuildFlags {
localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
isVerboseLoggingEnabled?: boolean;
whatToTest?: string;
}
export async function runBuildAndSubmitAsync({
graphqlClient,
analytics,
vcsClient,
projectDir,
flags,
actor,
getDynamicPrivateProjectConfigAsync,
downloadSimBuildAutoConfirm,
envOverride,
}: {
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
projectDir: string;
flags: BuildFlags;
actor: Actor;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
downloadSimBuildAutoConfirm?: boolean;
envOverride?: Env;
}): Promise<{
buildIds: string[];
buildProfiles?: ProfileData<'build'>[];
}> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<void> {
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: flags.nonInteractive,
vcsClient,
});
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
@@ -143,56 +117,26 @@ export async function runBuildAndSubmitAsync({
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
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/`
);
} else if (
buildProfile.profile.image &&
[
'ubuntu-20.04-jdk-11-ndk-r19c',
'ubuntu-20.04-jdk-8-ndk-r19c',
'ubuntu-20.04-jdk-11-ndk-r21e',
'ubuntu-20.04-jdk-8-ndk-r21e',
'ubuntu-22.04-jdk-8-ndk-r21e',
'ubuntu-20.04-jdk-11-ndk-r23b',
].includes(buildProfile.profile.image)
) {
Log.warn(
`The "image" field in the build profile "${buildProfile.profileName}" is set to "${
buildProfile.profile.image
}". This image is deprecated and will be removed on September 1st, 2024. ${learnMore(
'https://expo.dev/changelog/2024/07-12-eas-build-upcoming-android-images-updates'
)}`
);
}
}
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/')}`
);
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
vcsClient,
});
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
{};
for (const buildProfile of buildProfiles) {
await validateBuildProfileVersionSettingsAsync(
buildProfile,
easJsonCliConfig,
projectDir,
flags
);
const maybeMetadata = await validateCustomBuildConfigAsync({
projectDir,
profile: buildProfile.profile,
vcsClient,
});
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
const maybeMetadata = await validateCustomBuildConfigAsync(projectDir, buildProfile.profile);
if (maybeMetadata) {
customBuildConfigMetadataByPlatform[toAppPlatform(buildProfile.platform)] = maybeMetadata;
}
@@ -206,17 +150,6 @@ export async function runBuildAndSubmitAsync({
for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform);
const { env } = !envOverride
? await evaluateConfigWithEnvVarsAsync({
buildProfile: buildProfile.profile,
buildProfileName: buildProfile.profileName,
graphqlClient,
getProjectConfig: getDynamicPrivateProjectConfigAsync,
opts: { env: buildProfile.profile.env },
})
: { env: envOverride };
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir,
flags,
@@ -226,11 +159,8 @@ export async function runBuildAndSubmitAsync({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
easJsonAccessor,
});
if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -238,25 +168,13 @@ export async function runBuildAndSubmitAsync({
buildCtxByPlatform[platform] = buildCtx;
}
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
if (flags.localBuildOptions.localBuildMode) {
return;
}
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
const startedBuild = await BuildQuery.byIdAsync(
graphqlClient,
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();
}
Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine();
const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) {
@@ -265,7 +183,6 @@ export async function runBuildAndSubmitAsync({
platforms,
profileName: flags.submitProfile,
type: 'submit',
projectDir,
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
@@ -273,14 +190,14 @@ export async function runBuildAndSubmitAsync({
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
)
).profile;
const submission = await prepareAndStartAutoSubmissionAsync({
const submission = await prepareAndStartSubmissionAsync({
build: startedBuild.build,
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1,
projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile,
nonInteractive: flags.nonInteractive,
selectedSubmitProfileName: flags.submitProfile,
});
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
graphqlClient,
@@ -289,32 +206,24 @@ export async function runBuildAndSubmitAsync({
submissions.push(submission);
}
if (!flags.localBuildOptions.localBuildMode) {
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
}
if (flags.localBuildOptions.localBuildMode) {
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
Log.newLine();
printSubmissionDetailsUrls(submissions);
Log.newLine();
}
if (!flags.wait) {
if (flags.json) {
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
return;
}
const { accountName } = Object.values(buildCtxByPlatform)[0];
const builds = await waitForBuildEndAsync(graphqlClient, {
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
projectDir,
nonInteractive: flags.nonInteractive,
});
if (!flags.json) {
printBuildResults(builds);
@@ -326,7 +235,7 @@ export async function runBuildAndSubmitAsync({
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) {
@@ -349,10 +258,6 @@ export async function runBuildAndSubmitAsync({
}
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
}
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
}
async function prepareAndStartBuildAsync({
@@ -364,11 +269,8 @@ async function prepareAndStartBuildAsync({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
env,
easJsonAccessor,
}: {
projectDir: string;
flags: BuildFlags;
@@ -378,11 +280,8 @@ async function prepareAndStartBuildAsync({
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
easJsonAccessor: EasJsonAccessor;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName,
@@ -399,14 +298,8 @@ async function prepareAndStartBuildAsync({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
whatToTest: flags.whatToTest,
env,
});
if (moreBuilds) {
@@ -419,61 +312,22 @@ async function prepareAndStartBuildAsync({
);
}
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
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,
easJsonAccessor,
});
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
if (
isUsingEASUpdate(
buildCtx.exp,
buildCtx.projectId,
easJsonCliConfig.updateManifestHostOverride ?? null
)
) {
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);
if (
easJsonCliConfig?.appVersionSource === undefined &&
buildProfile.profile.autoIncrement !== 'version'
) {
if (buildProfile.profile.autoIncrement !== true) {
Log.warn(
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
'https://docs.expo.dev/build-reference/app-versions/'
)}`
);
} else {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
easJsonAccessor,
easJsonCliConfig,
flags.nonInteractive
);
}
}
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
}
@@ -505,21 +359,21 @@ async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragme
return await sendBuildRequestAsync();
}
async function prepareAndStartAutoSubmissionAsync({
async function prepareAndStartSubmissionAsync({
build,
buildCtx,
moreBuilds,
projectDir,
buildProfile,
submitProfile,
selectedSubmitProfileName,
nonInteractive,
}: {
build: BuildFragment;
buildCtx: BuildContext<Platform>;
moreBuilds: boolean;
projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile;
selectedSubmitProfileName?: string;
nonInteractive: boolean;
}): Promise<SubmissionFragment> {
const platform = toPlatform(build.platform);
@@ -529,7 +383,7 @@ async function prepareAndStartAutoSubmissionAsync({
profile: submitProfile,
archiveFlags: { id: build.id },
nonInteractive,
env: buildCtx.env,
env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user,
@@ -537,11 +391,6 @@ async function prepareAndStartAutoSubmissionAsync({
analytics: buildCtx.analytics,
projectId: buildCtx.projectId,
exp: buildCtx.exp,
vcsClient: buildCtx.vcsClient,
isVerboseFastlaneEnabled: false,
specifiedProfile: selectedSubmitProfileName,
groups: undefined, // use groups from submit profile
whatToTest: buildCtx.whatToTest,
});
if (moreBuilds) {
@@ -565,29 +414,18 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
}
}
export async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
assert(build.artifacts?.applicationArchiveUrl);
const cachedAppPath = getEasBuildRunCachedAppPath(build.project.id, build.id, build.platform);
if (await pathExists(cachedAppPath)) {
Log.newLine();
Log.log(`Using cached app...`);
await runAsync(cachedAppPath, build.platform);
return;
}
const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl,
build.platform,
cachedAppPath
build.platform
);
await runAsync(buildPath, build.platform);
}
async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[],
flags: BuildFlags,
autoConfirm?: boolean
flags: BuildFlags
): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
@@ -595,15 +433,11 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine();
const confirm =
autoConfirm ??
(await confirmAsync({
message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
}?`,
}));
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);
}
@@ -613,25 +447,15 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
}
async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp,
projectId,
projectDir,
vcsClient,
buildProfile,
nonInteractive,
sdkVersion,
env,
easJsonAccessor,
}: {
exp: ExpoConfig;
projectId: string;
projectDir: string;
vcsClient: Client;
buildProfile: ProfileData<'build'>;
nonInteractive: boolean;
sdkVersion?: string;
env: Env | undefined;
easJsonAccessor: EasJsonAccessor;
}): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return;
@@ -643,30 +467,18 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
);
} 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".`
`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".`
);
} 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".`
`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.`
);
const installExpoUpdates = await confirmAsync({
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
message: `Would you like to install the "expo-updates" package?`,
});
if (installExpoUpdates) {
const easJsonCliConfig: EasJson['cli'] =
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
await ensureEASUpdateIsConfiguredAsync({
exp,
projectId,
projectDir,
platform: RequestedPlatform.All,
vcsClient,
env,
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
await installExpoUpdatesAsync(projectDir, { silent: false });
Log.withTick('Installed expo-updates');
}
}
}
-23
View File
@@ -1,9 +1,3 @@
import { ResourceClass } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { LocalBuildOptions } from './local';
import { RequestedPlatform } from '../platform';
export enum BuildStatus {
NEW = 'new',
IN_QUEUE = 'in-queue',
@@ -17,22 +11,5 @@ export enum BuildStatus {
export enum BuildDistributionType {
STORE = 'store',
INTERNAL = 'internal',
/** @deprecated Use simulator flag instead */
SIMULATOR = 'simulator',
}
export interface BuildFlags {
requestedPlatform: RequestedPlatform;
profile?: string;
nonInteractive: boolean;
wait: boolean;
clearCache: boolean;
json: boolean;
autoSubmit: boolean;
submitProfile?: string;
localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass;
message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
}
+6 -22
View File
@@ -3,7 +3,6 @@ import { Errors } from '@oclif/core';
import chalk from 'chalk';
import resolveFrom from 'resolve-from';
import { reviewAndCommitChangesAsync } from './repository';
import { toAppPlatform } from '../../graphql/types/AppPlatform';
import Log, { learnMore } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
@@ -11,16 +10,15 @@ import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
import { getVcsClient } from '../../vcs';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
vcsClient,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
vcsClient: Client;
nonInteractive?: boolean;
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
@@ -43,7 +41,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform, vcsClient))
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
Log.newLine();
@@ -57,19 +55,6 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
'expo-dev-client'
)} 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);
if (areAllManaged) {
const install = await confirmAsync({
@@ -77,7 +62,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoDevClientAsync(projectDir, vcsClient, { nonInteractive });
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
@@ -115,7 +100,6 @@ async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolea
async function installExpoDevClientAsync(
projectDir: string,
vcsClient: Client,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
Log.newLine();
@@ -123,8 +107,8 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await vcsClient.isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync(vcsClient, 'Install expo-dev-client', {
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
@@ -1,26 +0,0 @@
import { BuildProfile } from '@expo/eas-json';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
type Environment = NonNullable<BuildProfile['environment']>;
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
production: EnvironmentVariableEnvironment.Production,
preview: EnvironmentVariableEnvironment.Preview,
development: EnvironmentVariableEnvironment.Development,
};
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
return Object.values(EnvironmentVariableEnvironment).includes(
env as EnvironmentVariableEnvironment
);
}
export function buildProfileEnvironmentToEnvironment(
environment: BuildProfile['environment']
): EnvironmentVariableEnvironment | null {
if (!environment) {
return null;
}
return BuildProfileEnvironmentToEnvironment[environment];
}
+10 -47
View File
@@ -1,49 +1,13 @@
import chalk from 'chalk';
import { getBuildLogsUrl } from './url';
import {
AppPlatform,
BuildFragment,
BuildStatus as GraphQLBuildStatus,
} from '../../graphql/generated';
import { link } from '../../log';
import { appPlatformDisplayNames } from '../../platform';
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
export function formatGraphQLBuildArtifacts(build: BuildFragment): FormatFieldsItem[] {
const fields = [
{
label: 'Application Archive URL',
get value() {
switch (build.status) {
case GraphQLBuildStatus.InProgress:
return '<in progress>';
default: {
const url = build.artifacts?.buildUrl;
return url ? link(url) : 'null';
}
}
},
},
{
label: 'Build Artifacts URL',
get value() {
switch (build.status) {
case GraphQLBuildStatus.InProgress:
return '<in progress>';
default: {
const url = build.artifacts?.buildArtifactsUrl;
return url ? link(url) : 'null';
}
}
},
},
];
return fields.filter(({ value }) => value !== undefined && value !== null) as {
label: string;
value: string;
}[];
}
import formatFields from '../../utils/formatFields';
import { getBuildLogsUrl } from './url';
export function formatGraphQLBuild(build: BuildFragment): string {
const actor = getActorName(build);
@@ -75,10 +39,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
}
},
},
{
label: 'Profile',
value: build.buildProfile,
},
{
label: 'Message',
value: build.message,
@@ -91,6 +51,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(),
},
{
label: 'Release Channel',
value: build.releaseChannel,
},
{
label: 'Channel',
value: build.channel,
@@ -117,11 +81,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
},
{
label: 'Logs',
value: link(getBuildLogsUrl(build)),
value: getBuildLogsUrl(build),
},
...formatGraphQLBuildArtifacts(build),
{
label: 'Fingerprint',
label: 'Artifact',
get value() {
switch (build.status) {
case GraphQLBuildStatus.New:
@@ -133,8 +96,8 @@ export function formatGraphQLBuild(build: BuildFragment): string {
case GraphQLBuildStatus.Errored:
return null;
case GraphQLBuildStatus.Finished: {
const hash = build.fingerprint?.hash;
return hash ? chalk.green(hash) : chalk.red('not found');
const url = build.artifacts?.buildUrl;
return url ? url : chalk.red('not found');
}
default:
return null;
@@ -2,7 +2,6 @@ import assert from 'assert';
import indentString from 'indent-string';
import qrcodeTerminal from 'qrcode-terminal';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
import {
AppPlatform,
BuildError,
@@ -14,6 +13,7 @@ import {
} from '../../graphql/generated';
import Log, { learnMore, link } from '../../log';
import { appPlatformDisplayNames, appPlatformEmojis } from '../../platform';
import { getBuildLogsUrl, getInternalDistributionInstallUrl } from './url';
function terminalLinkFallback(url: string, text: string): string {
return `${text} (${url})`;
@@ -43,14 +43,14 @@ const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) =>
export function printLogsUrls(builds: BuildFragment[]): void {
if (builds.length === 1) {
Log.log(`See logs: ${link(getBuildLogsUrl(builds[0]))}`);
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
} else {
builds.forEach(build => {
const logsUrl = getBuildLogsUrl(build);
Log.log(
`See ${appPlatformEmojis[build.platform]} ${
`${appPlatformEmojis[build.platform]} ${
appPlatformDisplayNames[build.platform]
} logs: ${link(logsUrl)}`
} build details: ${link(logsUrl)}`
);
});
}
@@ -62,9 +62,7 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
assert(build, 'Build should be defined');
printBuildResult(build);
} else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => {
printBuildResult(build);
});
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
}
}
@@ -99,9 +97,9 @@ function printBuildResult(build: BuildFragment): void {
// to the build details page and let people press the button to download there
const qrcodeUrl =
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code => {
Log.log(`${indentString(code, 2)}\n`);
});
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
Log.log(`${indentString(code, 2)}\n`)
);
Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform]
+21 -93
View File
@@ -4,19 +4,16 @@ import path from 'path';
import tar from 'tar';
import { v4 as uuidv4 } from 'uuid';
import Log, { learnMore } from '../../log';
import Log from '../../log';
import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { Client } from '../../vcs/vcs';
import { getVcsClient } from '../../vcs';
export async function maybeBailOnRepoStatusAsync(
vcsClient: Client,
nonInteractive: boolean
): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) {
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -31,20 +28,12 @@ export async function maybeBailOnRepoStatusAsync(
});
if (!answer) {
if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
}
throw new Error('Commit all changes. Aborting...');
}
}
export async function ensureRepoIsCleanAsync(
vcsClient: Client,
nonInteractive = false
): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) {
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -55,31 +44,25 @@ export async function ensureRepoIsCleanAsync(
)}.`
);
if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
throw new Error('Commit all changes. Aborting...');
}
const answer = await confirmAsync({
message: `Commit changes to git?`,
});
if (answer) {
await commitPromptAsync(vcsClient, { commitAllFiles: true });
await commitPromptAsync({ commitAllFiles: true });
} else {
throw new Error('Commit all changes. Aborting...');
}
}
export async function commitPromptAsync(
vcsClient: Client,
{
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}
): Promise<void> {
export async function commitPromptAsync({
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
@@ -87,40 +70,14 @@ export async function commitPromptAsync(
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await vcsClient.commitAsync({
await getVcsClient().commitAsync({
commitAllFiles,
commitMessage: message,
nonInteractive: false,
});
}
export type LocalFile = {
path: string;
size: number;
};
export async function makeProjectMetadataFileAsync(archivePath: string): Promise<LocalFile> {
await fs.mkdirp(getTmpDirectory());
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> {
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
@@ -142,7 +99,7 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
startTimer(compressTimerLabel);
try {
await vcsClient.makeShallowCopyAsync(shallowClonePath);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -169,34 +126,6 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
return { size, path: tarPath };
}
export function maybeWarnAboutProjectTarballSize(size: number): void {
if (size <= 150 /* MiB */ * 1024 /* KiB */ * 1024 /* B */) {
return;
}
Log.warn(
`Your project archive is ${formatBytes(
size
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
'.easignore'
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
);
}
const MAX_ALLOWED_PROJECT_TARBALL_SIZE =
2 /* GiB */ * 1024 /* MiB */ * 1024 /* KiB */ * 1024; /* B */
export function assertProjectTarballSizeDoesNotExceedLimit(size: number): void {
if (size <= MAX_ALLOWED_PROJECT_TARBALL_SIZE) {
return;
}
throw new Error(
`Project archive is too big. Maximum allowed size is ${formatBytes(
MAX_ALLOWED_PROJECT_TARBALL_SIZE
)}.`
);
}
enum ShouldCommitChanges {
Yes,
ShowDiffFirst,
@@ -204,12 +133,11 @@ enum ShouldCommitChanges {
}
export async function reviewAndCommitChangesAsync(
vcsClient: Client,
initialCommitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await vcsClient.commitAsync({
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
@@ -243,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."
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(vcsClient, { initialCommitMessage });
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await vcsClient.showDiffAsync();
await reviewAndCommitChangesAsync(vcsClient, initialCommitMessage, {
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
});
@@ -7,13 +7,21 @@ import Log, { link } from '../../log';
type AndroidResourceClass = Exclude<
ResourceClass,
ResourceClass.M1_MEDIUM | ResourceClass.M_MEDIUM | ResourceClass.M_LARGE
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
| ResourceClass.M_MEDIUM
| ResourceClass.M_LARGE
>;
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
@@ -49,7 +57,15 @@ export async function resolveBuildResourceClassAsync<T extends Platform>(
}
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
if (selectedResourceClass && ResourceClass.M1_MEDIUM === selectedResourceClass) {
if (
selectedResourceClass &&
[
ResourceClass.M1_EXPERIMENTAL,
ResourceClass.M1_MEDIUM,
ResourceClass.M1_LARGE,
ResourceClass.INTEL_MEDIUM,
].includes(selectedResourceClass)
) {
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
}
@@ -72,7 +88,7 @@ function resolveIosResourceClass(
);
}
if (ResourceClass.M1_MEDIUM === resourceClass) {
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'm-medium'
@@ -80,7 +96,7 @@ function resolveIosResourceClass(
);
}
if (ResourceClass.M_LARGE === resourceClass) {
if ([ResourceClass.M_LARGE, ResourceClass.M1_LARGE].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'large'
-31
View File
@@ -50,34 +50,3 @@ export function getUpdateGroupUrl(
getExpoWebsiteBaseUrl()
).toString();
}
export function getWorkflowRunUrl(
accountName: string,
projectName: string,
workflowRunId: string
): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/workflows/${workflowRunId}`,
getExpoWebsiteBaseUrl()
).toString();
}
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/github`,
getExpoWebsiteBaseUrl()
).toString();
}
export function getHostingDeploymentsUrl(accountName: string, projectName: string): string {
return new URL(
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
projectName
)}/hosting/deployments`,
getExpoWebsiteBaseUrl()
).toString();
}
+1 -1
View File
@@ -3,9 +3,9 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import semver from 'semver';
import { updateAppJsonConfigAsync } from './appJson';
import Log from '../../log';
import { promptAsync } from '../../prompts';
import { updateAppJsonConfigAsync } from './appJson';
export function ensureStaticConfigExists(projectDir: string): void {
const paths = getConfigFilePaths(projectDir);
+11 -14
View File
@@ -4,14 +4,15 @@ import fs from 'fs-extra';
import path from 'path';
import semver from 'semver';
import { CommonContext } from './context';
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 {
if (ctx.env?.NODE_ENV === 'production') {
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
Log.warn(
'You set NODE_ENV=production in the build profile or environment variables. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
);
Log.newLine();
}
@@ -27,28 +28,24 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const googleServicesEnvVar =
ctx.platform === Platform.ANDROID
? ctx.env.GOOGLE_SERVICES_JSON
: ctx.env.GOOGLE_SERVICES_INFO_PLIST;
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync(
path.relative(rootDir, absGoogleServicesFilePath)
))) &&
!googleServicesEnvVar
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
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.`
);
Log.warn(
`Use EAS file environment variables with secret or sensitive visibility to pass all values that you don't want to include in your version control to build process. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
)}`
);
Log.warn(
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
);
Log.newLine();
}
}
@@ -0,0 +1,8 @@
export class ListChannels {
constructor() {}
async runAsync(): Promise<void> {}
// TODO return channels fragment
async listChannelsAsync(): Promise<void> {}
}
@@ -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],
};
Loaded 100 of 635 files, more files were not shown because too many files have changed in this diff. Show more