Compare commits

..
Author SHA1 Message Date
Quinlan Jung 377c7a9ebb update CHANGELOG.md 2024-01-18 04:04:34 +00:00
Quinlan Jung ef3edcc787 [eas-cli] prompt user to map channel on update if unmapped 2024-01-17 19:50:02 -08:00
491 changed files with 5421 additions and 53305 deletions

No files matched your search

-1
View File
@@ -5,4 +5,3 @@
packages/eas-cli/src/graphql/generated.ts packages/eas-cli/src/graphql/generated.ts
node_modules node_modules
ts-declarations
+6
View File
@@ -30,6 +30,12 @@ module.exports = {
}, },
], ],
'no-underscore-dangle': ['error', { allow: ['__typename'] }], 'no-underscore-dangle': ['error', { allow: ['__typename'] }],
'@typescript-eslint/explicit-function-return-type': [
'warn',
{
allowExpressions: true,
},
],
'async-protect/async-suffix': 'error', 'async-protect/async-suffix': 'error',
'node/no-sync': 'error', 'node/no-sync': 'error',
}, },
-24
View File
@@ -1,24 +0,0 @@
commentConfiguration:
preamble: 'Subscribed to pull request'
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
rules:
- patterns: ['**/*']
mentions: ['khamilowicz', 'sjchmiela']
- 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']
+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: 20
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
working-directory: ./scripts
run: yarn install --frozen-lockfile --check-files
- name: Add changelog entry
working-directory: ./scripts
run: |
PARAMS=${PR_COMMENT_BODY#/changelog-entry }
if [[ ! "$PARAMS" =~ " " ]]; then
echo "Invalid /changelog-entry command"
exit 1
fi
CATEGORY=$( cut -d ' ' -f 1 <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
MESSAGE=$( cut -d ' ' -f 2- <<< "$PARAMS" | sed -e 's/^[[:space:]]*//' )
yarn changelog-entry "$CATEGORY" "$MESSAGE"
env:
GITHUB_PR_AUTHOR: ${{ github.event.issue.user.login }}
GITHUB_PR_NUMBER: ${{ github.event.issue.number }}
PR_COMMENT_BODY: ${{ github.event.comment.body }}
- name: Commit and push
run: |
# use the last commit author name and email
git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)"
git config --global user.email "$(git --no-pager log --format=format:'%ae' -n 1)"
git add CHANGELOG.md
git commit -m "update CHANGELOG.md"
git push
+3 -2
View File
@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Check if CHANGELOG.md was updated - name: Check if CHANGELOG.md was updated
id: changelog-updated id: changelog-updated
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076 uses: tj-actions/changed-files@v41
with: with:
files: CHANGELOG.md files: CHANGELOG.md
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent - name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
@@ -36,5 +36,6 @@ jobs:
refresh-message-position: true 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: ${{ 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: | 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. ⏩ 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 }}
+2 -23
View File
@@ -69,22 +69,16 @@ jobs:
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: 20 node-version: 20
- run: brew install python-setuptools
env:
HOMEBREW_NO_AUTO_UPDATE: 1
- run: yarn install --frozen-lockfile --check-files - run: yarn install --frozen-lockfile --check-files
- run: yarn build - run: yarn build
- name: Build tarballs - name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli working-directory: ${{ github.workspace }}/packages/eas-cli
run: | run: |
yarn pretarball-ci yarn pretarball-ci
yarn oclif pack:tarballs --no-xz --targets darwin-x64,darwin-arm64 yarn oclif pack:tarballs --no-xz --targets darwin-x64
- id: x64 - id: x64
run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT run: echo "filename=$(ls eas-*-x64.tar.gz)" >> $GITHUB_OUTPUT
working-directory: ${{ github.workspace }}/packages/eas-cli/dist 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 - name: Upload darwin-x64 tarball
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
@@ -94,15 +88,6 @@ jobs:
asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }} asset_path: packages/eas-cli/dist/${{ steps.x64.outputs.filename }}
asset_name: ${{ steps.x64.outputs.filename }} asset_name: ${{ steps.x64.outputs.filename }}
asset_content_type: application/gzip asset_content_type: application/gzip
- name: Upload darwin-arm64 tarball
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.release.outputs.upload_url }}
asset_path: packages/eas-cli/dist/${{ steps.arm64.outputs.filename }}
asset_name: ${{ steps.arm64.outputs.filename }}
asset_content_type: application/gzip
build-windows: build-windows:
name: Build for Windows name: Build for Windows
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -167,13 +152,7 @@ jobs:
VERSION=$(cat lerna.json | jq -r .version) VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
env: echo "Run \"npm dist-tag add eas-cli@$VERSION latest-eas-build\" to promote it to production."
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
- name: Add latest-eas-build tag
run: |
VERSION=$(cat lerna.json | jq -r .version)
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
npm dist-tag add eas-cli@$VERSION latest-eas-build
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
release-changelog: release-changelog:
+2 -5
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
node: ['18', '20', '22'] node: ['16', '18', '20']
include: include:
- node: '20' - node: '20'
coverage: true coverage: true
@@ -28,9 +28,6 @@ jobs:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- run: yarn install --frozen-lockfile --check-files - run: yarn install --frozen-lockfile --check-files
- run: yarn typecheck - run: yarn typecheck
- run: |
git config --global user.email "[email protected]"
git config --global user.name "Test"
- run: yarn test - run: yarn test
if: ${{ !matrix.coverage }} if: ${{ !matrix.coverage }}
env: env:
@@ -52,7 +49,7 @@ jobs:
notify-slack: notify-slack:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test] needs: test
name: Notify Slack name: Notify Slack
if: ${{ github.ref == 'refs/heads/main' && always() }} if: ${{ github.ref == 'refs/heads/main' && always() }}
steps: steps:
@@ -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: 'radoslawkrzemien,sjchmiela,khamilowicz'
github_token: ${{ secrets.EXPO_BOT_PAT }}
- uses: actions/github-script@v3
with:
github-token: ${{ secrets.EXPO_BOT_PAT }}
script: |
github.issues.createComment({
issue_number: ${{ steps.open-pr.outputs.pr_number }},
owner: context.repo.owner,
repo: context.repo.repo,
body: "/changelog-entry chore Upgrade [`eas-build`](https://github.com/expo/eas-build) dependencies."
})
-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,
}
}
+3 -991
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
+4
View File
@@ -12,3 +12,7 @@ The algorithm works as follows:
- If there are any entries in the "🛠 Breaking changes" section, bump the MAJOR version. - 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, if there are any entries in the "🎉 New features", bump the MINOR version.
- Otherwise, bump the PATCH 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`.
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"npmClient": "yarn", "npmClient": "yarn",
"version": "16.3.3", "version": "6.0.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json" "$schema": "node_modules/lerna/schemas/lerna-schema.json"
} }
+1 -1
View File
@@ -23,7 +23,7 @@
"@types/jest": "29.5.11", "@types/jest": "29.5.11",
"@types/node": "20.11.0", "@types/node": "20.11.0",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-universe": "13.0.0", "eslint-config-universe": "12.0.0",
"eslint-plugin-async-protect": "3.1.0", "eslint-plugin-async-protect": "3.1.0",
"eslint-plugin-node": "11.1.0", "eslint-plugin-node": "11.1.0",
"jest": "29.7.0", "jest": "29.7.0",
@@ -1,3 +0,0 @@
# eas-build-cache-provider
A build cache provider plugin for the Expo CLI
@@ -1,58 +0,0 @@
{
"name": "eas-build-cache-provider",
"description": "A build cache provider plugin for the Expo CLI",
"version": "0.1.0",
"author": "Expo <[email protected]>",
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/config": "10.0.6",
"@babel/code-frame": "7.23.5",
"@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,41 +0,0 @@
import { getPackageJson } from '@expo/config';
import { SpawnResult } from '@expo/spawn-async';
// import from '@expo/config';
type RunOptions = any;
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,147 +0,0 @@
import { Platform } 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';
// import from '@expo/config';
type CalculateFingerprintHashProps = any;
type RemoteBuildCachePlugin = any;
type RunOptions = any;
async function resolveRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
runOptions,
}: {
projectRoot: string;
platform: Platform;
fingerprintHash: string;
runOptions: RunOptions;
}): 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 remote build cache, upgrade your eas-cli installation to latest.`
);
}
return null;
}
}
async function uploadEASRemoteBuildCacheAsync({
projectRoot,
platform,
fingerprintHash,
buildPath,
}: {
projectRoot: string;
runOptions: RunOptions;
platform: Platform;
fingerprintHash: string;
buildPath?: string;
}): 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 remote cache}`);
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 EASRemoteBuildCacheProvider: RemoteBuildCachePlugin = {
resolveRemoteBuildCache: resolveRemoteBuildCacheAsync,
uploadRemoteBuildCache: uploadEASRemoteBuildCacheAsync,
calculateFingerprintHash: calculateEASFingerprintHashAsync,
};
export default EASRemoteBuildCacheProvider;
@@ -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/**/*"]
}
+181 -840
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(); (fs.realpath as any).native = jest.fn();
module.exports = fs; 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);
-1
View File
@@ -7,7 +7,6 @@ documents:
- 'src/commands/**/*.ts' - 'src/commands/**/*.ts'
- 'src/branch/**/*.ts' - 'src/branch/**/*.ts'
- 'src/channel/**/*.ts' - 'src/channel/**/*.ts'
- 'src/worker/**/*.ts'
generates: generates:
src/graphql/generated.ts: src/graphql/generated.ts:
plugins: plugins:
+1631 -23184
View File
File diff suppressed because it is too large. Load diff
+23 -40
View File
@@ -1,34 +1,33 @@
{ {
"name": "eas-cli", "name": "eas-cli",
"description": "EAS command line tool", "description": "EAS command line tool",
"version": "16.3.3", "version": "6.0.0",
"author": "Expo <[email protected]>", "author": "Expo <[email protected]>",
"bin": { "bin": {
"eas": "./bin/run" "eas": "./bin/run"
}, },
"bugs": "https://github.com/expo/eas-cli/issues", "bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": { "dependencies": {
"@expo/apple-utils": "2.1.12", "@expo/apple-utils": "1.3.2",
"@expo/code-signing-certificates": "0.0.5", "@expo/code-signing-certificates": "0.0.5",
"@expo/config": "10.0.6", "@expo/config": "8.1.2",
"@expo/config-plugins": "9.0.12", "@expo/config-plugins": "7.2.4",
"@expo/eas-build-job": "1.0.173", "@expo/config-types": "49.0.0",
"@expo/eas-json": "16.3.3", "@expo/eas-build-job": "1.0.56",
"@expo/env": "^1.0.0", "@expo/eas-json": "6.0.0",
"@expo/json-file": "8.3.3", "@expo/json-file": "8.2.37",
"@expo/logger": "1.0.117", "@expo/multipart-body-parser": "1.1.0",
"@expo/multipart-body-parser": "2.0.0", "@expo/osascript": "2.0.33",
"@expo/osascript": "2.1.4", "@expo/package-manager": "1.1.2",
"@expo/package-manager": "1.7.0", "@expo/pkcs12": "0.0.8",
"@expo/pkcs12": "0.1.3", "@expo/plist": "0.0.20",
"@expo/plist": "0.2.0",
"@expo/plugin-help": "5.1.23", "@expo/plugin-help": "5.1.23",
"@expo/plugin-warn-if-update-available": "2.5.1", "@expo/plugin-warn-if-update-available": "2.5.1",
"@expo/prebuild-config": "8.0.17", "@expo/prebuild-config": "6.2.5",
"@expo/results": "1.0.0", "@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1", "@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.2", "@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.173", "@expo/steps": "1.0.51",
"@expo/timeago.js": "1.0.0", "@expo/timeago.js": "1.0.0",
"@oclif/core": "^1.26.2", "@oclif/core": "^1.26.2",
"@oclif/plugin-autocomplete": "^2.3.10", "@oclif/plugin-autocomplete": "^2.3.10",
@@ -41,7 +40,6 @@
"chalk": "4.1.2", "chalk": "4.1.2",
"cli-progress": "3.12.0", "cli-progress": "3.12.0",
"dateformat": "4.6.3", "dateformat": "4.6.3",
"diff": "7.0.0",
"dotenv": "16.3.1", "dotenv": "16.3.1",
"env-paths": "2.2.0", "env-paths": "2.2.0",
"envinfo": "7.11.0", "envinfo": "7.11.0",
@@ -64,11 +62,9 @@
"log-symbols": "4.1.0", "log-symbols": "4.1.0",
"mime": "3.0.0", "mime": "3.0.0",
"minimatch": "5.1.2", "minimatch": "5.1.2",
"minizlib": "3.0.1", "nanoid": "3.3.4",
"nanoid": "3.3.8",
"node-fetch": "2.6.7", "node-fetch": "2.6.7",
"node-forge": "1.3.1", "node-forge": "1.3.1",
"node-stream-zip": "1.15.0",
"nullthrows": "1.1.1", "nullthrows": "1.1.1",
"ora": "5.1.0", "ora": "5.1.0",
"pkg-dir": "4.2.0", "pkg-dir": "4.2.0",
@@ -79,28 +75,22 @@
"qrcode-terminal": "0.12.0", "qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0", "resolve-from": "5.0.0",
"semver": "7.5.4", "semver": "7.5.4",
"set-interval-async": "3.0.3",
"slash": "3.0.0", "slash": "3.0.0",
"tar": "6.2.1", "tar": "6.2.0",
"tar-stream": "3.1.7",
"terminal-link": "2.1.1", "terminal-link": "2.1.1",
"tslib": "2.6.2", "tslib": "2.6.2",
"turndown": "7.1.2", "turndown": "7.1.2",
"untildify": "4.0.0", "untildify": "4.0.0",
"uuid": "9.0.1", "uuid": "9.0.1",
"wrap-ansi": "7.0.0", "wrap-ansi": "7.0.0"
"yaml": "2.6.0",
"zod": "^3.23.8"
}, },
"devDependencies": { "devDependencies": {
"@graphql-codegen/cli": "5.0.0", "@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/introspection": "4.0.0", "@graphql-codegen/introspection": "4.0.0",
"@graphql-codegen/typescript": "4.0.1", "@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1", "@graphql-codegen/typescript-operations": "4.0.1",
"@tsconfig/node18": "18.2.4",
"@types/cli-progress": "3.11.5", "@types/cli-progress": "3.11.5",
"@types/dateformat": "3.0.1", "@types/dateformat": "3.0.1",
"@types/diff": "6.0.0",
"@types/envinfo": "7.8.3", "@types/envinfo": "7.8.3",
"@types/express": "4.17.15", "@types/express": "4.17.15",
"@types/fs-extra": "11.0.4", "@types/fs-extra": "11.0.4",
@@ -114,13 +104,12 @@
"@types/prompts": "2.4.2", "@types/prompts": "2.4.2",
"@types/semver": "7.5.6", "@types/semver": "7.5.6",
"@types/tar": "6.1.10", "@types/tar": "6.1.10",
"@types/tar-stream": "3.1.3",
"@types/tough-cookie": "4.0.2", "@types/tough-cookie": "4.0.2",
"@types/uuid": "9.0.7", "@types/uuid": "9.0.7",
"@types/wrap-ansi": "3.0.0", "@types/wrap-ansi": "3.0.0",
"axios": "0.28.1", "axios": "0.27.2",
"eslint-plugin-graphql": "4.0.0", "eslint-plugin-graphql": "4.0.0",
"express": "4.20.0", "express": "4.18.2",
"memfs": "3.4.13", "memfs": "3.4.13",
"mockdate": "3.0.5", "mockdate": "3.0.5",
"nock": "13.4.0", "nock": "13.4.0",
@@ -131,7 +120,7 @@
"typescript": "5.3.3" "typescript": "5.3.3"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=16.0.0"
}, },
"files": [ "files": [
"/bin", "/bin",
@@ -182,22 +171,16 @@
"secret": { "secret": {
"description": "manage project and account secrets" "description": "manage project and account secrets"
}, },
"env": {
"description": "manage project and account environment variables"
},
"update": { "update": {
"description": "manage individual updates" "description": "manage individual updates"
}, },
"webhook": { "webhook": {
"description": "manage webhooks" "description": "manage webhooks"
},
"workflow": {
"description": "manage workflows"
} }
}, },
"warn-if-update-available": { "warn-if-update-available": {
"timeoutInDays": 0.5, "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": { "update": {
"node": { "node": {
+17 -52
View File
@@ -1,20 +1,6 @@
{ {
"$schema": "http://json-schema.org/draft-06/schema", "$schema": "http://json-schema.org/draft-06/schema",
"type": "object", "type": "object",
"properties": {
"configVersion": {
"enum": [0],
"description": "The EAS metadata store configuration version"
},
"apple": {
"$ref": "#/definitions/AppleConfig"
}
},
"additionalProperties": false,
"required": [
"configVersion",
"apple"
],
"definitions": { "definitions": {
"apple": { "apple": {
"AppleKidsAge": { "AppleKidsAge": {
@@ -337,8 +323,7 @@
"gambling", "gambling",
"unrestrictedWebAccess", "unrestrictedWebAccess",
"kidsAgeBand", "kidsAgeBand",
"ageRatingOverride", "seventeenPlus"
"koreaAgeRatingOverride"
], ],
"defaultSnippets": [ "defaultSnippets": [
{ {
@@ -360,9 +345,7 @@
"gambling": false, "gambling": false,
"unrestrictedWebAccess": false, "unrestrictedWebAccess": false,
"kidsAgeBand": null, "kidsAgeBand": null,
"ageRatingOverride": "NONE", "seventeenPlus": false
"koreaAgeRatingOverride": "NONE",
"lootBox": false
} }
}, },
{ {
@@ -384,9 +367,7 @@
"unrestrictedWebAccess": false, "unrestrictedWebAccess": false,
"gambling": false, "gambling": false,
"kidsAgeBand": null, "kidsAgeBand": null,
"ageRatingOverride": "${13|NONE,SEVENTEEN_PLUS,UNRATED|}", "seventeenPlus": false
"koreaAgeRatingOverride": "${14|NONE,FIFTEEN_PLUS,NINETEEN_PLUS|}",
"lootBox": false
} }
} }
], ],
@@ -461,24 +442,8 @@
}, },
"seventeenPlus": { "seventeenPlus": {
"type": "boolean", "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", "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)" "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 "maxLength": 4000
} }
} }
},
"AppleAgeRatingOverride": {
"enum": [
"NONE",
"SEVENTEEN_PLUS",
"UNRATED"
]
},
"AppleKoreaAgeRatingOverride": {
"enum": [
"NONE",
"FIFTEEN_PLUS",
"NINETEEN_PLUS"
]
} }
}, },
"AppleConfig": { "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 () => { (async () => {
// Patch `oclif readme` path and link generation // Patch `oclif readme` path and link generation
let readmeContent = await fs.readFile('README.md', 'utf8'); let readmeContent = await fs.readFile('README.md', 'utf8');
readmeContent = readmeContent.replace(/src\/commands/g, 'packages/eas-cli/src/commands'); readmeContent = readmeContent.replace(/\[build\//g, '[src/');
readmeContent = readmeContent.replace(/build\/commands/g, 'packages/eas-cli/src/commands');
await fs.writeFile('README.md', readmeContent); await fs.writeFile('README.md', readmeContent);
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
-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 expoApiV2ErrorDetails?: JSONValue;
readonly expoApiV2ErrorServerStack?: string; readonly expoApiV2ErrorServerStack?: string;
readonly expoApiV2ErrorMetadata?: object; readonly expoApiV2ErrorMetadata?: object;
readonly expoApiV2ErrorRequestId?: string;
constructor(response: { constructor(response: {
message: string; message: string;
@@ -14,13 +13,11 @@ export class ApiV2Error extends Error {
stack?: string; stack?: string;
details?: JSONValue; details?: JSONValue;
metadata?: object; metadata?: object;
requestId?: string;
}) { }) {
super(response.message); super(response.message);
this.expoApiV2ErrorCode = response.code; this.expoApiV2ErrorCode = response.code;
this.expoApiV2ErrorDetails = response.details; this.expoApiV2ErrorDetails = response.details;
this.expoApiV2ErrorServerStack = response.stack; this.expoApiV2ErrorServerStack = response.stack;
this.expoApiV2ErrorMetadata = response.metadata; this.expoApiV2ErrorMetadata = response.metadata;
this.expoApiV2ErrorRequestId = response.requestId;
} }
} }
@@ -16,7 +16,6 @@ describe(ApiV2Client, () => {
stack: 'line 1: hello', stack: 'line 1: hello',
details: { who: 'world' }, details: { who: 'world' },
metadata: { an: 'object' }, metadata: { an: 'object' },
requestId: 'request-id',
}, },
], ],
}); });
@@ -38,7 +37,6 @@ describe(ApiV2Client, () => {
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' }); expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' }); expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello'); expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
}); });
it('does not convert non-APIv2 error to ApiV2Error', async () => { it('does not convert non-APIv2 error to ApiV2Error', async () => {
@@ -1,5 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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."`; 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,18 +1,10 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import { import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
getMockEasJson,
mockCommandContext,
mockProjectId,
mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils';
import BuildVersionGetView from '../../commands/build/version/get'; import BuildVersionGetView from '../../commands/build/version/get';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log'; import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json'; import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
jest.mock('../../project/applicationIdentifier'); jest.mock('../../project/applicationIdentifier');
@@ -20,7 +12,16 @@ jest.mock('fs');
jest.mock('../../log'); jest.mock('../../log');
jest.mock('../../utils/json'); jest.mock('../../utils/json');
jest.mock('../../graphql/queries/AppVersionQuery'); jest.mock('../../graphql/queries/AppVersionQuery');
jest.mock('../../prompts');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionGetView, () => { describe(BuildVersionGetView, () => {
afterEach(() => { afterEach(() => {
@@ -48,29 +49,6 @@ describe(BuildVersionGetView, () => {
expect(printJsonOnlyOutput).not.toHaveBeenCalled(); 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 () => { test('reading version for platform android when no remote version is set', async () => {
const ctx = mockCommandContext(BuildVersionGetView, { const ctx = mockCommandContext(BuildVersionGetView, {
easJson: withRemoteVersionSource(getMockEasJson()), easJson: withRemoteVersionSource(getMockEasJson()),
@@ -167,9 +145,7 @@ describe(BuildVersionGetView, () => {
}); });
test('reading version when appVersionSource is set to local ', async () => { test('reading version when appVersionSource is set to local ', async () => {
const ctx = mockCommandContext(BuildVersionGetView, { const ctx = mockCommandContext(BuildVersionGetView, {});
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({ jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
buildVersion: '100', buildVersion: '100',
storeVersion: '1.0.0', storeVersion: '1.0.0',
@@ -182,51 +158,4 @@ describe(BuildVersionGetView, () => {
); );
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot(); 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',
storeVersion: '1.0.0',
}));
const cmd = mockTestCommand(
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.`
);
});
}); });
@@ -1,3 +1,4 @@
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
@@ -7,15 +8,12 @@ import {
mockCommandContext, mockCommandContext,
mockProjectId, mockProjectId,
mockTestCommand, mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils'; } from './utils';
import BuildVersionSetView from '../../commands/build/version/set'; import BuildVersionSetView from '../../commands/build/version/set';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation'; import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppQuery } from '../../graphql/queries/AppQuery'; import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log from '../../log'; import Log from '../../log';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import * as prompts from '../../prompts'; import * as prompts from '../../prompts';
jest.mock('../../project/applicationIdentifier'); jest.mock('../../project/applicationIdentifier');
@@ -27,6 +25,16 @@ jest.mock('../../log');
jest.mock('../../prompts'); jest.mock('../../prompts');
jest.mock('../../utils/json'); jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSetView, () => { describe(BuildVersionSetView, () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); 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 () => { test('printing current remote version before prompting for a new one', async () => {
const ctx = mockCommandContext(BuildVersionSetView, { const ctx = mockCommandContext(BuildVersionSetView, {
easJson: withRemoteVersionSource(getMockEasJson()), 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 () => { test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, { const ctx = mockCommandContext(BuildVersionSetView, {});
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false); jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
@@ -147,24 +125,8 @@ describe(BuildVersionSetView, () => {
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith(); 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 () => { test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSetView, { const ctx = mockCommandContext(BuildVersionSetView, {});
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true); jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
@@ -175,33 +137,4 @@ describe(BuildVersionSetView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); 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,4 +1,5 @@
import { Workflow } from '@expo/eas-build-job'; import { Workflow } from '@expo/eas-build-job';
import { AppVersionSource, EasJson } from '@expo/eas-json';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
@@ -8,8 +9,6 @@ import {
mockCommandContext, mockCommandContext,
mockProjectId, mockProjectId,
mockTestCommand, mockTestCommand,
withLocalVersionSource,
withRemoteVersionSource,
} from './utils'; } from './utils';
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version'; import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/version';
import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version'; import { updateNativeVersionsAsync as updateIosNativeVersionsAsync } from '../../build/ios/version';
@@ -20,7 +19,6 @@ import { AppQuery } from '../../graphql/queries/AppQuery';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import { getAppBuildGradleAsync } from '../../project/android/gradleUtils'; import { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
import { resolveTargetsAsync } from '../../project/ios/target'; import { resolveTargetsAsync } from '../../project/ios/target';
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import * as prompts from '../../prompts'; import * as prompts from '../../prompts';
@@ -39,6 +37,16 @@ jest.mock('../../log');
jest.mock('../../prompts'); jest.mock('../../prompts');
jest.mock('../../utils/json'); jest.mock('../../utils/json');
function withRemoteVersionSource(easJson: EasJson): EasJson {
return {
...easJson,
cli: {
...easJson.cli,
appVersionSource: AppVersionSource.REMOTE,
},
};
}
describe(BuildVersionSyncView, () => { describe(BuildVersionSyncView, () => {
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -71,34 +79,6 @@ describe(BuildVersionSyncView, () => {
expect(syncAndroidAsync).not.toHaveBeenCalled(); 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 () => { test('syncing version for bare project on platform android', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, { const ctx = mockCommandContext(BuildVersionSyncView, {
easJson: withRemoteVersionSource(getMockEasJson()), 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 () => { 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, {}); const ctx = mockCommandContext(BuildVersionSyncView, {});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest
.mocked(prompts.selectAsync)
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false); jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx); 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 () => { test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
const ctx = mockCommandContext(BuildVersionSyncView, { const ctx = mockCommandContext(BuildVersionSyncView, {});
easJson: withLocalVersionSource(getMockEasJson()),
});
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null); jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment()); jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true); jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
@@ -239,33 +201,4 @@ describe(BuildVersionSyncView, () => {
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json')); const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote'); 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 { ExpoConfig } from '@expo/config-types';
import { AppVersionSource, EasJson } from '@expo/eas-json'; import { EasJson } from '@expo/eas-json';
import { Command, Config } from '@oclif/core'; import { Command, Config } from '@oclif/core';
import { vol } from 'memfs'; import { vol } from 'memfs';
import path from 'path'; import path from 'path';
@@ -24,7 +24,6 @@ export function getMockAppFragment(): AppFragment {
return { return {
id: mockProjectId, id: mockProjectId,
slug: 'testapp', slug: 'testapp',
name: 'testapp',
fullName: '@testuser/testpp', fullName: '@testuser/testpp',
ownerAccount: { ownerAccount: {
id: 'test-account-id', id: 'test-account-id',
@@ -148,23 +147,3 @@ export const getError = <TError = any>(call: () => unknown): TError | NoErrorThr
return error as TError; 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,
},
};
}
@@ -28,7 +28,7 @@ import {
*/ */
export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> { export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> {
constructor( constructor(
private readonly options: { private options: {
filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean; filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean;
printedType?: string; printedType?: string;
pageSize?: number; pageSize?: number;
@@ -91,8 +91,8 @@ export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragme
queryParams: QueryParams queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> => ): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
filterPredicate filterPredicate
? await this.filterQueryAsync(ctx, queryParams, filterPredicate) ? this.filterQueryAsync(ctx, queryParams, filterPredicate)
: await this.queryAsync(ctx, queryParams); : this.queryAsync(ctx, queryParams);
const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> => const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> =>
branchInfo.name; branchInfo.name;
+9 -14
View File
@@ -1,5 +1,4 @@
import chalk from 'chalk'; import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag'; import gql from 'graphql-tag';
import { BranchNotFoundError } from './utils'; import { BranchNotFoundError } from './utils';
@@ -9,11 +8,10 @@ import { withErrorHandlingAsync } from '../graphql/client';
import { import {
CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutation,
CreateUpdateBranchForAppMutationVariables, CreateUpdateBranchForAppMutationVariables,
UpdateBranchBasicInfoFragment, UpdateBranch,
UpdateBranchFragment, UpdateBranchFragment,
} from '../graphql/generated'; } from '../graphql/generated';
import { BranchQuery } from '../graphql/queries/BranchQuery'; import { BranchQuery } from '../graphql/queries/BranchQuery';
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
import Log from '../log'; import Log from '../log';
import { formatBranch, getBranchDescription } from '../update/utils'; import { formatBranch, getBranchDescription } from '../update/utils';
import { printJsonOnlyOutput } from '../utils/json'; import { printJsonOnlyOutput } from '../utils/json';
@@ -86,9 +84,7 @@ export async function listAndRenderBranchesOnAppAsync(
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId), queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
promptOptions: { promptOptions: {
title: 'Load more branches?', title: 'Load more branches?',
renderListItems: branches => { renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
renderPageOfBranches(branches, paginatedQueryOptions);
},
}, },
}); });
} }
@@ -128,7 +124,7 @@ function renderPageOfBranches(
export async function createUpdateBranchOnAppAsync( export async function createUpdateBranchOnAppAsync(
graphqlClient: ExpoGraphqlClient, graphqlClient: ExpoGraphqlClient,
{ appId, name }: CreateUpdateBranchForAppMutationVariables { appId, name }: CreateUpdateBranchForAppMutationVariables
): Promise<UpdateBranchBasicInfoFragment> { ): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
const result = await withErrorHandlingAsync( const result = await withErrorHandlingAsync(
graphqlClient graphqlClient
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>( .mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
@@ -137,17 +133,15 @@ export async function createUpdateBranchOnAppAsync(
updateBranch { updateBranch {
createUpdateBranchForApp(appId: $appId, name: $name) { createUpdateBranchForApp(appId: $appId, name: $name) {
id id
...UpdateBranchBasicInfoFragment name
} }
} }
} }
${print(UpdateBranchBasicInfoFragmentNode)}
`, `,
{ {
appId, appId,
name, name,
}, }
{ additionalTypenames: ['UpdateBranch'] }
) )
.toPromise() .toPromise()
); );
@@ -167,21 +161,22 @@ export async function ensureBranchExistsAsync(
appId: string; appId: string;
branchName: string; branchName: string;
} }
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> { ): Promise<{ branchId: string; createdBranch: boolean }> {
try { try {
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, { const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId, appId,
name: branchName, name: branchName,
}); });
return { branch: updateBranch, createdBranch: false }; const { id } = updateBranch;
return { branchId: id, createdBranch: false };
} catch (error) { } catch (error) {
if (error instanceof BranchNotFoundError) { if (error instanceof BranchNotFoundError) {
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, { const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId, appId,
name: branchName, name: branchName,
}); });
return { branch: newUpdateBranch, createdBranch: true }; return { branchId: newUpdateBranch.id, createdBranch: true };
} else { } else {
throw error; throw error;
} }
+4 -2
View File
@@ -1,7 +1,9 @@
import { Client } from '../vcs/vcs'; import { Client } from '../vcs/vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> { export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
return await vcsClient.getBranchNameAsync(); return (
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
);
} }
export class BranchNotFoundError extends Error { export class BranchNotFoundError extends Error {
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToCancelAsync } from '../../commands/build/cancel'; import { selectBuildToCancelAsync } from '../../commands/build/cancel';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated'; import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery'; import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts'; import { confirmAsync, selectAsync } from '../../prompts';
@@ -47,14 +53,14 @@ describe(selectBuildToCancelAsync.name, () => {
it('does not return build id when confirmation is rejected', async () => { it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>()); const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false); 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 () => { it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>()); const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId); jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true); jest.mocked(confirmAsync).mockResolvedValueOnce(true);
await expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual( expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId selectedBuildId
); );
}); });
@@ -85,6 +91,6 @@ function createMockBuildFragment({
}, },
}, },
status: BuildStatus.InQueue, 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);
});
});
@@ -3,7 +3,13 @@ import { v4 as uuid } from 'uuid';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient'; import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { selectBuildToDeleteAsync } from '../../commands/build/delete'; import { selectBuildToDeleteAsync } from '../../commands/build/delete';
import { AppPlatform, BuildFragment, BuildPriority, BuildStatus } from '../../graphql/generated'; import {
AppPlatform,
BuildFragment,
BuildPriority,
BuildResourceClass,
BuildStatus,
} from '../../graphql/generated';
import { BuildQuery } from '../../graphql/queries/BuildQuery'; import { BuildQuery } from '../../graphql/queries/BuildQuery';
import { confirmAsync, selectAsync } from '../../prompts'; import { confirmAsync, selectAsync } from '../../prompts';
@@ -47,14 +53,14 @@ describe(selectBuildToDeleteAsync.name, () => {
it('does not return build id when confirmation is rejected', async () => { it('does not return build id when confirmation is rejected', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>()); const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(confirmAsync).mockResolvedValueOnce(false); jest.mocked(confirmAsync).mockResolvedValueOnce(false);
await expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null); expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
}); });
it('returns build id when confirmation is confirmed', async () => { it('returns build id when confirmation is confirmed', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>()); const graphqlClient = instance(mock<ExpoGraphqlClient>());
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId); jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
jest.mocked(confirmAsync).mockResolvedValueOnce(true); jest.mocked(confirmAsync).mockResolvedValueOnce(true);
await expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual( expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
selectedBuildId selectedBuildId
); );
}); });
@@ -85,6 +91,6 @@ function createMockBuildFragment({
}, },
}, },
status: BuildStatus.InQueue, status: BuildStatus.InQueue,
isForIosSimulator: false, resourceClass: BuildResourceClass.AndroidMedium,
}; };
} }
@@ -1,45 +1,25 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { Platform } from '@expo/eas-build-job'; import { Platform } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import { AndroidBuildProfile } from '@expo/eas-json/build/build/types';
import assert from 'assert'; import assert from 'assert';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { vol } from 'memfs'; import { vol } from 'memfs';
import os from 'os'; import os from 'os';
import path from 'path'; 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 { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { resolveVcsClient } from '../../../vcs'; import { getVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import { import {
BumpStrategy, BumpStrategy,
bumpVersionAsync, bumpVersionAsync,
bumpVersionInAppJsonAsync, bumpVersionInAppJsonAsync,
maybeResolveVersionsAsync, maybeResolveVersionsAsync,
resolveRemoteVersionCodeAsync,
} from '../version'; } from '../version';
const fsReal = jest.requireActual('fs').promises as typeof fs; const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('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(); const vcsClient = getVcsClient();
afterAll(async () => { afterAll(async () => {
// do not remove the following line // do not remove the following line
@@ -236,160 +216,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({ function initBareWorkflowProject({
versionCode = 123, versionCode = 123,
versionName = '3.0.0', versionName = '3.0.0',
}: { }: {
versionCode?: number | null; versionCode?: number;
versionName?: string | null; versionName?: string;
} = {}): ExpoConfig { } = {}): ExpoConfig {
const fakeExp: ExpoConfig = { const fakeExp: ExpoConfig = {
name: 'myproject', name: 'myproject',
@@ -407,8 +239,8 @@ function initBareWorkflowProject({
'./android/app/build.gradle': `android { './android/app/build.gradle': `android {
defaultConfig { defaultConfig {
applicationId "com.expo.testapp" applicationId "com.expo.testapp"
${versionCode ? `versionCode "${versionCode}"` : ''} versionCode ${versionCode}
${versionName ? `versionName "${versionName}"` : ''} versionName "${versionName}"
} }
}`, }`,
'./android/app/src/main/AndroidManifest.xml': 'fake', './android/app/src/main/AndroidManifest.xml': 'fake',
+4 -5
View File
@@ -1,4 +1,4 @@
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job'; import { Android, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json'; import { AppVersionSource } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
@@ -99,21 +99,21 @@ export async function prepareAndroidBuildAsync(
return await ensureAndroidCredentialsAsync(ctx); return await ensureAndroidCredentialsAsync(ctx);
}, },
syncProjectConfigurationAsync: async () => { syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({ await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir, projectDir: ctx.projectDir,
exp: ctx.exp, exp: ctx.exp,
localAutoIncrement: localAutoIncrement:
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false ? false
: ctx.buildProfile.autoIncrement, : ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
vcsClient: ctx.vcsClient, vcsClient: ctx.vcsClient,
env: ctx.env,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
ctx: BuildContext<Platform.ANDROID>, ctx: BuildContext<Platform.ANDROID>,
jobData: JobData<AndroidCredentials> jobData: JobData<AndroidCredentials>
): Promise<Android.Job> => { ): Promise<Job> => {
return await prepareJobAsync(ctx, jobData); return await prepareJobAsync(ctx, jobData);
}, },
sendBuildRequestAsync: async ( sendBuildRequestAsync: async (
@@ -151,7 +151,6 @@ async function ensureAndroidCredentialsAsync(
ctx.android.gradleContext ctx.android.gradleContext
); );
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, { const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
name: ctx.buildProfile.keystoreName,
app: { app: {
account: nullthrows( account: nullthrows(
ctx.user.accounts.find(a => a.name === ctx.accountName), ctx.user.accounts.find(a => a.name === ctx.accountName),
@@ -2,13 +2,11 @@ import { Android } from '@expo/eas-build-job';
import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated'; import { AndroidBuildType, AndroidJobInput } from '../../graphql/generated';
import { import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode, transformBuildMode,
transformBuildTrigger, transformBuildTrigger,
transformProjectArchive, transformProjectArchive,
transformWorkflow, transformWorkflow,
} from '../graphql'; } from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Android.Job): AndroidJobInput { export function transformJob(job: Android.Job): AndroidJobInput {
return { return {
@@ -16,6 +14,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy), triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive), projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: job.projectRootDirectory, projectRootDirectory: job.projectRootDirectory,
releaseChannel: job.releaseChannel,
updates: job.updates, updates: job.updates,
secrets: job.secrets, secrets: job.secrets,
builderEnvironment: job.builderEnvironment, builderEnvironment: job.builderEnvironment,
@@ -30,10 +29,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
experimental: job.experimental, experimental: job.experimental,
mode: transformBuildMode(job.mode), mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig, customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
}; };
} }
@@ -3,15 +3,16 @@ import {
ArchiveSource, ArchiveSource,
BuildMode, BuildMode,
BuildTrigger, BuildTrigger,
Job,
Platform, Platform,
sanitizeBuildJob, sanitizeJob,
} from '@expo/eas-build-job'; } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import path from 'path'; import path from 'path';
import slash from 'slash'; import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider'; import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig'; import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -28,7 +29,7 @@ const cacheDefaults = {
export async function prepareJobAsync( export async function prepareJobAsync(
ctx: BuildContext<Platform.ANDROID>, ctx: BuildContext<Platform.ANDROID>,
jobData: JobData jobData: JobData
): Promise<Android.Job> { ): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user); const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile; const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory = const projectRootDirectory =
@@ -53,18 +54,9 @@ export async function prepareJobAsync(
} }
const maybeCustomBuildConfigPath = buildProfile.config const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPathForJob(buildProfile.config) ? getCustomBuildConfigPath(buildProfile.config)
: undefined; : undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
}
const job: Android.Job = { const job: Android.Job = {
type: ctx.workflow, type: ctx.workflow,
platform: Platform.ANDROID, platform: Platform.ANDROID,
@@ -73,15 +65,12 @@ export async function prepareJobAsync(
builderEnvironment: { builderEnvironment: {
image: buildProfile.image, image: buildProfile.image,
node: buildProfile.node, node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm, pnpm: buildProfile.pnpm,
bun: buildProfile.bun, bun: buildProfile.bun,
yarn: buildProfile.yarn, yarn: buildProfile.yarn,
ndk: buildProfile.ndk, ndk: buildProfile.ndk,
env: { expoCli: buildProfile.expoCli,
...buildProfile.env, env: buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
}, },
cache: { cache: {
...cacheDefaults, ...cacheDefaults,
@@ -91,12 +80,12 @@ export async function prepareJobAsync(
secrets: { secrets: {
...buildCredentials, ...buildCredentials,
}, },
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel }, updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient, developmentClient: buildProfile.developmentClient,
gradleCommand: buildProfile.gradleCommand, gradleCommand: buildProfile.gradleCommand,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath, applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths, buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
buildType, buildType,
username, username,
...(ctx.android.versionCodeOverride && { ...(ctx.android.versionCodeOverride && {
@@ -107,24 +96,14 @@ export async function prepareJobAsync(
experimental: { experimental: {
prebuildCommand: buildProfile.prebuildCommand, prebuildCommand: buildProfile.prebuildCommand,
}, },
mode: buildMode, mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI, triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && { ...(maybeCustomBuildConfigPath && {
customBuildConfig: { customBuildConfig: {
path: maybeCustomBuildConfigPath, path: maybeCustomBuildConfigPath,
}, },
}), }),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
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,42 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { AndroidConfig } from '@expo/config-plugins'; import { AndroidConfig } from '@expo/config-plugins';
import { Env, Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { AndroidVersionAutoIncrement } from '@expo/eas-json'; import { AndroidVersionAutoIncrement } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version'; import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import Log from '../../log'; import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils'; import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule'; import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { Client } from '../../vcs/vcs'; import { Client } from '../../vcs/vcs';
export async function syncProjectConfigurationAsync({ export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir, projectDir,
exp, exp,
localAutoIncrement, localAutoIncrement,
projectId,
vcsClient, vcsClient,
env,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement; localAutoIncrement?: AndroidVersionAutoIncrement;
projectId: string;
vcsClient: Client; vcsClient: Client;
env: Env | undefined; }
}): Promise<void> { ): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
await cleanUpOldEasBuildGradleScriptAsync(projectDir); await cleanUpOldEasBuildGradleScriptAsync(projectDir);
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env }); await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
} else { } else {
+6 -22
View File
@@ -44,7 +44,7 @@ export async function bumpVersionAsync({
const buildGradle = await getAppBuildGradleAsync(projectDir); const buildGradle = await getAppBuildGradleAsync(projectDir);
const isMultiFlavor = const isMultiFlavor =
buildGradle.android?.productFlavors ?? buildGradle.android?.flavorDimensions; buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
if (isMultiFlavor) { if (isMultiFlavor) {
throw new Error( throw new Error(
'Automatic version bumping is not supported for multi-flavor Android projects.' 'Automatic version bumping is not supported for multi-flavor Android projects.'
@@ -99,7 +99,7 @@ export async function maybeResolveVersionsAsync(
exp: ExpoConfig, exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>, buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client vcsClient: Client
): Promise<{ appVersion?: string; appBuildVersion?: string; isVersionInitialized?: boolean }> { ): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir); const buildGradle = await getAppBuildGradleAsync(projectDir);
@@ -113,11 +113,6 @@ export async function maybeResolveVersionsAsync(
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0', resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
appBuildVersion: appBuildVersion:
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1', resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
isVersionInitialized: !resolveConfigValue(
buildGradle,
'versionCode',
parsedGradleCommand?.flavor
),
}; };
} catch { } catch {
return {}; return {};
@@ -209,18 +204,14 @@ export async function resolveRemoteVersionCodeAsync(
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient); const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
let currentBuildVersion: string; let currentBuildVersion: string;
let shouldInitializeVersionCode = false;
if (remoteVersions?.buildVersion) { if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion; currentBuildVersion = remoteVersions.buildVersion;
} else { } else {
if (localVersions.appBuildVersion) { if (localVersions.appBuildVersion) {
Log.log( Log.warn(
chalk.green(
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.' 'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
)
); );
currentBuildVersion = localVersions.appBuildVersion; currentBuildVersion = localVersions.appBuildVersion;
shouldInitializeVersionCode = localVersions.isVersionInitialized ?? false;
} else { } else {
Log.error( 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.` `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 +221,7 @@ export async function resolveRemoteVersionCodeAsync(
} }
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) { if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion; return currentBuildVersion;
} else if ( } else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeVersionCode
) {
const spinner = ora( const spinner = ora(
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.` `Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
).start(); ).start();
@@ -244,9 +232,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationIdentifier: applicationId, applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0', storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: currentBuildVersion, buildVersion: currentBuildVersion,
runtimeVersion: runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
}); });
spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`); spinner.succeed(`Initialized versionCode with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) { } catch (err) {
@@ -268,9 +254,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationIdentifier: applicationId, applicationIdentifier: applicationId,
storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0', storeVersion: localVersions.appVersion ?? exp.version ?? '1.0.0',
buildVersion: String(nextBuildVersion), buildVersion: String(nextBuildVersion),
runtimeVersion: runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.ANDROID) ?? undefined,
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.ANDROID)) ??
undefined,
}); });
spinner.succeed( spinner.succeed(
`Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold( `Incremented versionCode from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
+41 -200
View File
@@ -1,11 +1,4 @@
import { import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
ArchiveSource,
ArchiveSourceType,
BuildJob,
FingerprintSource,
Metadata,
Platform,
} from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json'; import { CredentialsSource } from '@expo/eas-json';
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
@@ -33,20 +26,12 @@ import { transformMetadata } from './graphql';
import { LocalBuildMode, runLocalBuildAsync } from './local'; import { LocalBuildMode, runLocalBuildAsync } from './local';
import { collectMetadataAsync } from './metadata'; import { collectMetadataAsync } from './metadata';
import { printDeprecationWarnings } from './utils/printBuildInfo'; import { printDeprecationWarnings } from './utils/printBuildInfo';
import { import { makeProjectTarballAsync, reviewAndCommitChangesAsync } from './utils/repository';
LocalFile,
assertProjectTarballSizeDoesNotExceedLimit,
makeProjectMetadataFileAsync,
makeProjectTarballAsync,
maybeWarnAboutProjectTarballSize,
reviewAndCommitChangesAsync,
} from './utils/repository';
import { BuildEvent } from '../analytics/AnalyticsManager'; import { BuildEvent } from '../analytics/AnalyticsManager';
import { withAnalyticsAsync } from '../analytics/common'; import { withAnalyticsAsync } from '../analytics/common';
import { getExpoWebsiteBaseUrl } from '../api'; import { getExpoWebsiteBaseUrl } from '../api';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient'; import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { EasCommandError } from '../commandUtils/errors'; import { EasCommandError } from '../commandUtils/errors';
import { createFingerprintAsync } from '../fingerprint/cli';
import { import {
AppPlatform, AppPlatform,
BuildFragment, BuildFragment,
@@ -60,13 +45,10 @@ import { BuildQuery } from '../graphql/queries/BuildQuery';
import Log, { learnMore, link } from '../log'; import Log, { learnMore, link } from '../log';
import { Ora, ora } from '../ora'; import { Ora, ora } from '../ora';
import { import {
RequestedPlatform,
appPlatformDisplayNames, appPlatformDisplayNames,
appPlatformEmojis, appPlatformEmojis,
requestedPlatformDisplayNames, requestedPlatformDisplayNames,
} from '../platform'; } from '../platform';
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
import { uploadFileAtPathToGCSAsync } from '../uploads'; import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files'; import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json'; import { printJsonOnlyOutput } from '../utils/json';
@@ -83,14 +65,14 @@ export interface JobData<Credentials> {
projectArchive: ArchiveSource; projectArchive: ArchiveSource;
} }
interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob> { interface Builder<TPlatform extends Platform, Credentials, TJob extends Job> {
ctx: BuildContext<TPlatform>; ctx: BuildContext<TPlatform>;
ensureCredentialsAsync( ensureCredentialsAsync(
ctx: BuildContext<TPlatform> ctx: BuildContext<TPlatform>
): Promise<CredentialsResult<Credentials> | undefined>; ): Promise<CredentialsResult<Credentials> | undefined>;
syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>; syncProjectConfigurationAsync(ctx: BuildContext<TPlatform>): Promise<void>;
prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<BuildJob>; prepareJobAsync(ctx: BuildContext<TPlatform>, jobData: JobData<Credentials>): Promise<Job>;
sendBuildRequestAsync( sendBuildRequestAsync(
appId: string, appId: string,
job: TJob, job: TJob,
@@ -115,7 +97,7 @@ function resolveBuildParamsInput<T extends Platform>(
export async function prepareBuildRequestForPlatformAsync< export async function prepareBuildRequestForPlatformAsync<
TPlatform extends Platform, TPlatform extends Platform,
Credentials, Credentials,
TJob extends BuildJob, TJob extends Job,
>(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> { >(builder: Builder<TPlatform, Credentials, TJob>): Promise<BuildRequestSender> {
const { ctx } = builder; const { ctx } = builder;
const credentialsResult = await withAnalyticsAsync( const credentialsResult = await withAnalyticsAsync(
@@ -131,9 +113,7 @@ export async function prepareBuildRequestForPlatformAsync<
await withAnalyticsAsync( await withAnalyticsAsync(
ctx.analytics, ctx.analytics,
async () => { async () => await builder.syncProjectConfigurationAsync(ctx),
await builder.syncProjectConfigurationAsync(ctx);
},
{ {
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT, attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS, successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
@@ -144,25 +124,18 @@ export async function prepareBuildRequestForPlatformAsync<
if (await ctx.vcsClient.isCommitRequiredAsync()) { if (await ctx.vcsClient.isCommitRequiredAsync()) {
Log.addNewLineIfNone(); Log.addNewLineIfNone();
const platformToRequestedPlatform: Record<Platform, RequestedPlatform> = {
[Platform.ANDROID]: RequestedPlatform.Android,
[Platform.IOS]: RequestedPlatform.Ios,
};
await reviewAndCommitChangesAsync( await reviewAndCommitChangesAsync(
ctx.vcsClient, ctx.vcsClient,
`[EAS Build] Run EAS Build for ${ `[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
requestedPlatformDisplayNames[platformToRequestedPlatform[ctx.platform]]
}`,
{ nonInteractive: ctx.nonInteractive } { nonInteractive: ctx.nonInteractive }
); );
} }
let projectArchive: ArchiveSource | undefined; let projectArchive: ArchiveSource | undefined;
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) { if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
const projectPath = (await makeProjectTarballAsync(ctx.vcsClient)).path;
projectArchive = { projectArchive = {
type: ArchiveSourceType.PATH, type: ArchiveSourceType.PATH,
path: projectPath, path: (await makeProjectTarballAsync(ctx.vcsClient)).path,
}; };
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) { } else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = { projectArchive = {
@@ -172,14 +145,12 @@ export async function prepareBuildRequestForPlatformAsync<
} else if (!ctx.localBuildOptions.localBuildMode) { } else if (!ctx.localBuildOptions.localBuildMode) {
projectArchive = { projectArchive = {
type: ArchiveSourceType.GCS, type: ArchiveSourceType.GCS,
...(await uploadProjectAsync(ctx)), bucketKey: await uploadProjectAsync(ctx),
}; };
} }
assert(projectArchive); assert(projectArchive);
const runtimeAndFingerprintMetadata = const metadata = await collectMetadataAsync(ctx);
await computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync(ctx);
const metadata = await collectMetadataAsync(ctx, runtimeAndFingerprintMetadata);
const buildParams = resolveBuildParamsInput(ctx, metadata); const buildParams = resolveBuildParamsInput(ctx, metadata);
const job = await builder.prepareJobAsync(ctx, { const job = await builder.prepareJobAsync(ctx, {
projectArchive, projectArchive,
@@ -188,7 +159,7 @@ export async function prepareBuildRequestForPlatformAsync<
return async () => { return async () => {
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) { if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env); await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
return undefined; return undefined;
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) { } else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, { await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
@@ -260,10 +231,7 @@ export function handleBuildRequestError(error: any, platform: Platform): never {
async function uploadProjectAsync<TPlatform extends Platform>( async function uploadProjectAsync<TPlatform extends Platform>(
ctx: BuildContext<TPlatform> ctx: BuildContext<TPlatform>
): Promise<{ ): Promise<string> {
bucketKey: string;
metadataLocation?: string;
}> {
let projectTarballPath; let projectTarballPath;
try { try {
return await withAnalyticsAsync( return await withAnalyticsAsync(
@@ -277,31 +245,36 @@ async function uploadProjectAsync<TPlatform extends Platform>(
); );
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient); const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
maybeWarnAboutProjectTarballSize(projectTarball.size); if (projectTarball.size > 1024 * 1024 * 100) {
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size); 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; projectTarballPath = projectTarball.path;
const [bucketKey, { metadataLocation }] = await Promise.all([
uploadFileAtPathToGCSAsync( const bucketKey = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient, ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectSources, UploadSessionType.EasBuildGcsProjectSources,
projectTarball.path, projectTarball.path,
createProgressTracker({ createProgressTracker({
total: projectTarball.size, total: projectTarball.size,
message: ratio => message: ratio =>
`Uploading to EAS Build (${formatBytes( `Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
projectTarball.size * ratio projectTarball.size
)} / ${formatBytes(projectTarball.size)})`, )})`,
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`, completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
}) })
), );
uploadMetadataFileAsync<TPlatform>(projectTarball, ctx), return bucketKey;
]);
if (metadataLocation) {
return { bucketKey, metadataLocation };
}
return { bucketKey };
}, },
{ {
attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT, attemptEvent: BuildEvent.PROJECT_UPLOAD_ATTEMPT,
@@ -312,11 +285,9 @@ async function uploadProjectAsync<TPlatform extends Platform>(
); );
} catch (err: any) { } catch (err: any) {
let errMessage = 'Failed to upload the project tarball to EAS Build'; let errMessage = 'Failed to upload the project tarball to EAS Build';
if (err.message) { if (err.message) {
errMessage += `\n\nReason: ${err.message}`; errMessage += `\n\nReason: ${err.message}`;
} }
throw new EasBuildProjectArchiveUploadError(errMessage); throw new EasBuildProjectArchiveUploadError(errMessage);
} finally { } finally {
if (projectTarballPath) { if (projectTarballPath) {
@@ -325,41 +296,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
} }
} }
async function uploadMetadataFileAsync<TPlatform extends Platform>( async function sendBuildRequestAsync<TPlatform extends Platform, Credentials, TJob extends Job>(
projectTarball: LocalFile,
ctx: BuildContext<TPlatform>
): Promise<{ metadataLocation: string | null }> {
let projectMetadataFile: LocalFile | null = null;
try {
projectMetadataFile = await makeProjectMetadataFileAsync(projectTarball.path);
const metadataLocation = await uploadFileAtPathToGCSAsync(
ctx.graphqlClient,
UploadSessionType.EasBuildGcsProjectMetadata,
projectMetadataFile.path
);
return { metadataLocation };
} catch (err: any) {
let errMessage = 'Failed to upload metadata to EAS Build';
if (err.message) {
errMessage += `\n\nReason: ${err.message}`;
}
Log.warn(errMessage);
return { metadataLocation: null };
} finally {
if (projectMetadataFile) {
await fs.remove(projectMetadataFile.path);
}
}
}
async function sendBuildRequestAsync<
TPlatform extends Platform,
Credentials,
TJob extends BuildJob,
>(
builder: Builder<TPlatform, Credentials, TJob>, builder: Builder<TPlatform, Credentials, TJob>,
job: TJob, job: TJob,
metadata: Metadata, metadata: Metadata,
@@ -479,8 +416,8 @@ async function handleSingleBuildProgressAsync(
spinner.succeed('Build finished'); spinner.succeed('Build finished');
return { refetch: false }; return { refetch: false };
case BuildStatus.New: case BuildStatus.New:
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link( spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
formatAccountBillingUrl(accountName) formatAccountSubscriptionsUrl(accountName)
)}.`; )}.`;
break; break;
case BuildStatus.InQueue: { case BuildStatus.InQueue: {
@@ -502,7 +439,7 @@ async function handleSingleBuildProgressAsync(
Log.log('Start builds sooner in the priority queue.'); Log.log('Start builds sooner in the priority queue.');
Log.log( Log.log(
`Sign up for EAS Production or Enterprise at ${link( `Sign up for EAS Production or Enterprise at ${link(
formatAccountBillingUrl(accountName) formatAccountSubscriptionsUrl(accountName)
)}` )}`
); );
} }
@@ -649,105 +586,9 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
} }
} }
function formatAccountBillingUrl(accountName: string): string { function formatAccountSubscriptionsUrl(accountName: string): string {
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString(); return new URL(
} `/accounts/${accountName}/settings/subscriptions`,
getExpoWebsiteBaseUrl()
async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends Platform>( ).toString();
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 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,
});
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,
};
} }
+3 -79
View File
@@ -1,10 +1,9 @@
import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json'; import { EasJson, EasJsonAccessor } from '@expo/eas-json';
import chalk from 'chalk'; import chalk from 'chalk';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository'; import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
import Log, { learnMore } from '../log'; import Log, { learnMore } from '../log';
import { ora } from '../ora';
import { easCliVersion } from '../utils/easCli'; import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs'; import { Client } from '../vcs/vcs';
@@ -41,7 +40,7 @@ async function configureAsync({
nonInteractive, nonInteractive,
vcsClient, vcsClient,
}: ConfigureParams): Promise<void> { }: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync(vcsClient, nonInteractive); await maybeBailOnRepoStatusAsync(vcsClient);
await createEasJsonAsync(projectDir, vcsClient); await createEasJsonAsync(projectDir, vcsClient);
@@ -53,82 +52,9 @@ async function configureAsync({
} }
} }
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_DEFAULT: EasJson = {
cli: { cli: {
version: `>= ${easCliVersion}`, version: `>= ${easCliVersion}`,
appVersionSource: AppVersionSource.REMOTE,
}, },
build: { build: {
development: { development: {
@@ -138,9 +64,7 @@ const EAS_JSON_DEFAULT: EasJson = {
preview: { preview: {
distribution: 'internal', distribution: 'internal',
}, },
production: { production: {},
autoIncrement: true,
},
}, },
submit: { submit: {
production: {}, production: {},
-5
View File
@@ -1,7 +1,6 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json'; import { BuildProfile, EasJson } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import { NodePackageManager } from '@expo/package-manager'; import { NodePackageManager } from '@expo/package-manager';
import { LocalBuildOptions } from './local'; import { LocalBuildOptions } from './local';
@@ -61,8 +60,4 @@ export interface BuildContext<T extends Platform> {
developmentClient: boolean; developmentClient: boolean;
requiredPackageManager: NodePackageManager['name'] | null; requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client; vcsClient: Client;
loggerLevel?: LoggerLevel;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
} }
+3 -22
View File
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job'; import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json'; import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
import JsonFile from '@expo/json-file'; import JsonFile from '@expo/json-file';
import { LoggerLevel } from '@expo/logger';
import { resolvePackageManager } from '@expo/package-manager'; import { resolvePackageManager } from '@expo/package-manager';
import getenv from 'getenv'; import getenv from 'getenv';
import resolveFrom from 'resolve-from'; import resolveFrom from 'resolve-from';
@@ -10,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid';
import { createAndroidContextAsync } from './android/build'; import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context'; import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build'; import { createIosContextAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local'; import { LocalBuildOptions } from './local';
import { resolveBuildResourceClassAsync } from './utils/resourceClass'; import { resolveBuildResourceClassAsync } from './utils/resourceClass';
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager'; import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField'; import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
@@ -40,11 +39,6 @@ export async function createBuildContextAsync<T extends Platform>({
vcsClient, vcsClient,
getDynamicPrivateProjectConfigAsync, getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata, customBuildConfigMetadata,
buildLoggerLevel,
freezeCredentials,
repack,
isVerboseLoggingEnabled,
env,
}: { }: {
buildProfileName: string; buildProfileName: string;
buildProfile: BuildProfile<T>; buildProfile: BuildProfile<T>;
@@ -63,15 +57,8 @@ export async function createBuildContextAsync<T extends Platform>({
vcsClient: Client; vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn; getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata; customBuildConfigMetadata?: CustomBuildConfigMetadata;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled: boolean;
env: Record<string, string>;
}): Promise<BuildContext<T>> { }): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
env,
});
const projectName = exp.slug; const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId); const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
@@ -93,10 +80,9 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor, user: actor,
graphqlClient, graphqlClient,
analytics, analytics,
env, env: buildProfile.env,
easJsonCliConfig, easJsonCliConfig,
vcsClient, vcsClient,
freezeCredentials,
}); });
const devClientProperties = getDevClientEventProperties({ const devClientProperties = getDevClientEventProperties({
@@ -114,7 +100,6 @@ export async function createBuildContextAsync<T extends Platform>({
...devClientProperties, ...devClientProperties,
no_wait: noWait, no_wait: noWait,
run_from_ci: runFromCI, run_from_ci: runFromCI,
local: localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN,
}; };
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties); analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
@@ -151,10 +136,6 @@ export async function createBuildContextAsync<T extends Platform>({
customBuildConfigMetadata, customBuildConfigMetadata,
developmentClient, developmentClient,
requiredPackageManager, requiredPackageManager,
loggerLevel: buildLoggerLevel,
repack,
isVerboseLoggingEnabled,
env,
}; };
if (platform === Platform.ANDROID) { if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>; const common = commonContext as CommonContext<Platform.ANDROID>;
@@ -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;
}
-32
View File
@@ -3,11 +3,9 @@ import {
ArchiveSourceType, ArchiveSourceType,
BuildMode, BuildMode,
BuildTrigger, BuildTrigger,
FingerprintSourceType,
Metadata, Metadata,
Workflow, Workflow,
} from '@expo/eas-build-job'; } from '@expo/eas-build-job';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert'; import assert from 'assert';
import { import {
@@ -16,13 +14,10 @@ import {
BuildMetadataInput, BuildMetadataInput,
BuildWorkflow, BuildWorkflow,
DistributionType, DistributionType,
FingerprintSourceInput,
BuildMode as GraphQLBuildMode, BuildMode as GraphQLBuildMode,
BuildTrigger as GraphQLBuildTrigger, BuildTrigger as GraphQLBuildTrigger,
FingerprintSourceType as GraphQLFingeprintSourceType,
ProjectArchiveSourceInput, ProjectArchiveSourceInput,
ProjectArchiveSourceType, ProjectArchiveSourceType,
WorkerLoggerLevel,
} from '../graphql/generated'; } from '../graphql/generated';
export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput { export function transformProjectArchive(archiveSource: ArchiveSource): ProjectArchiveSourceInput {
@@ -30,7 +25,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
return { return {
type: ProjectArchiveSourceType.Gcs, type: ProjectArchiveSourceType.Gcs,
bucketKey: archiveSource.bucketKey, bucketKey: archiveSource.bucketKey,
metadataLocation: archiveSource.metadataLocation,
}; };
} else if (archiveSource.type === ArchiveSourceType.URL) { } else if (archiveSource.type === ArchiveSourceType.URL) {
return { return {
@@ -45,8 +39,6 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
export function transformMetadata(metadata: Metadata): BuildMetadataInput { export function transformMetadata(metadata: Metadata): BuildMetadataInput {
return { return {
...metadata, ...metadata,
fingerprintSource:
metadata.fingerprintSource && transformFingerprintSource(metadata.fingerprintSource),
credentialsSource: credentialsSource:
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource), metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
distribution: metadata.distribution && transformDistribution(metadata.distribution), distribution: metadata.distribution && transformDistribution(metadata.distribution),
@@ -57,20 +49,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( function transformCredentialsSource(
credentialsSource: Metadata['credentialsSource'] credentialsSource: Metadata['credentialsSource']
): BuildCredentialsSource { ): BuildCredentialsSource {
@@ -113,7 +91,6 @@ const buildModeToGraphQLBuildMode: Record<BuildMode, GraphQLBuildMode> = {
[BuildMode.BUILD]: GraphQLBuildMode.Build, [BuildMode.BUILD]: GraphQLBuildMode.Build,
[BuildMode.CUSTOM]: GraphQLBuildMode.Custom, [BuildMode.CUSTOM]: GraphQLBuildMode.Custom,
[BuildMode.RESIGN]: GraphQLBuildMode.Resign, [BuildMode.RESIGN]: GraphQLBuildMode.Resign,
[BuildMode.REPACK]: GraphQLBuildMode.Repack,
}; };
export function transformBuildMode(buildMode: BuildMode): GraphQLBuildMode { export function transformBuildMode(buildMode: BuildMode): GraphQLBuildMode {
@@ -130,12 +107,3 @@ export function transformBuildTrigger(buildTrigger: BuildTrigger): GraphQLBuildT
} }
throw new Error('Unknown build trigger'); 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,12 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins'; import { IOSConfig } from '@expo/config-plugins';
import { IosBuildProfile } from '@expo/eas-json/build/build/types';
import fs from 'fs-extra'; import fs from 'fs-extra';
import { vol } from 'memfs'; import { vol } from 'memfs';
import os from 'os'; import os from 'os';
import { instance, mock } from 'ts-mockito';
import type { XCBuildConfiguration } from 'xcode'; 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 { readPlistAsync } from '../../../utils/plist';
import { resolveVcsClient } from '../../../vcs'; import { getVcsClient } from '../../../vcs';
import { Client } from '../../../vcs/vcs';
import { import {
BumpStrategy, BumpStrategy,
bumpVersionAsync, bumpVersionAsync,
@@ -22,25 +15,11 @@ import {
getInfoPlistPath, getInfoPlistPath,
readBuildNumberAsync, readBuildNumberAsync,
readShortVersionAsync, readShortVersionAsync,
resolveRemoteBuildNumberAsync,
} from '../version'; } from '../version';
jest.mock('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(); const vcsClient = getVcsClient();
const getXCBuildConfigurationFromPbxproj = jest.spyOn( const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target, IOSConfig.Target,
@@ -321,6 +300,25 @@ describe(readShortVersionAsync, () => {
); );
expect(buildNumber).toBe('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',
},
vcsClient
)
).rejects.toThrowError(
'CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: 0.0.7.1.028'
);
});
}); });
describe('managed project', () => { describe('managed project', () => {
@@ -380,154 +378,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({ function initBareWorkflowProject({
appVersion = '1.0.0', appVersion = '1.0.0',
buildNumber = '1', buildNumber = '1',
+7 -9
View File
@@ -1,4 +1,4 @@
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job'; import { Ios, Job, Metadata, Platform, Workflow } from '@expo/eas-build-job';
import { AppVersionSource } from '@expo/eas-json'; import { AppVersionSource } from '@expo/eas-json';
import { ensureIosCredentialsAsync } from './credentials'; import { ensureIosCredentialsAsync } from './credentials';
@@ -10,7 +10,6 @@ import { IosCredentials } from '../../credentials/ios/types';
import { BuildParamsInput } from '../../graphql/generated'; import { BuildParamsInput } from '../../graphql/generated';
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation'; import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier'; import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme'; import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target'; import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build'; import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
@@ -25,11 +24,10 @@ import {
export async function createIosContextAsync( export async function createIosContextAsync(
ctx: CommonContext<Platform.IOS> ctx: CommonContext<Platform.IOS>
): Promise<IosBuildContext> { ): Promise<IosBuildContext> {
const { buildProfile, env } = ctx; const { buildProfile } = ctx;
if (ctx.workflow === Workflow.MANAGED) { if (ctx.workflow === Workflow.MANAGED) {
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx); await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
} }
checkNodeEnvVariable(ctx); checkNodeEnvVariable(ctx);
@@ -49,7 +47,7 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir, projectDir: ctx.projectDir,
exp: ctx.exp, exp: ctx.exp,
xcodeBuildContext, xcodeBuildContext,
env, env: buildProfile.env,
vcsClient: ctx.vcsClient, vcsClient: ctx.vcsClient,
}); });
const applicationTarget = findApplicationTarget(targets); const applicationTarget = findApplicationTarget(targets);
@@ -79,10 +77,10 @@ export async function prepareIosBuildAsync(
return await prepareBuildRequestForPlatformAsync({ return await prepareBuildRequestForPlatformAsync({
ctx, ctx,
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => { ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
return await ensureIosCredentialsAsync(ctx, ctx.ios.targets); return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
}, },
syncProjectConfigurationAsync: async () => { syncProjectConfigurationAsync: async () => {
await syncProjectConfigurationAsync({ await syncProjectConfigurationAsync(ctx.graphqlClient, {
projectDir: ctx.projectDir, projectDir: ctx.projectDir,
exp: ctx.exp, exp: ctx.exp,
targets: ctx.ios.targets, targets: ctx.ios.targets,
@@ -90,14 +88,14 @@ export async function prepareIosBuildAsync(
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? false ? false
: ctx.buildProfile.autoIncrement, : ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
vcsClient: ctx.vcsClient, vcsClient: ctx.vcsClient,
env: ctx.env,
}); });
}, },
prepareJobAsync: async ( prepareJobAsync: async (
ctx: BuildContext<Platform.IOS>, ctx: BuildContext<Platform.IOS>,
jobData: JobData<IosCredentials> jobData: JobData<IosCredentials>
): Promise<Ios.Job> => { ): Promise<Job> => {
return await prepareJobAsync(ctx, { return await prepareJobAsync(ctx, {
...jobData, ...jobData,
buildScheme: ctx.ios.xcodeBuildContext.buildScheme, buildScheme: ctx.ios.xcodeBuildContext.buildScheme,
+1 -6
View File
@@ -3,13 +3,11 @@ import nullthrows from 'nullthrows';
import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated'; import { IosJobInput, IosJobSecretsInput } from '../../graphql/generated';
import { import {
loggerLevelToGraphQLWorkerLoggerLevel,
transformBuildMode, transformBuildMode,
transformBuildTrigger, transformBuildTrigger,
transformProjectArchive, transformProjectArchive,
transformWorkflow, transformWorkflow,
} from '../graphql'; } from '../graphql';
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
export function transformJob(job: Ios.Job): IosJobInput { export function transformJob(job: Ios.Job): IosJobInput {
return { return {
@@ -17,6 +15,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
triggeredBy: transformBuildTrigger(job.triggeredBy), triggeredBy: transformBuildTrigger(job.triggeredBy),
projectArchive: transformProjectArchive(job.projectArchive), projectArchive: transformProjectArchive(job.projectArchive),
projectRootDirectory: nullthrows(job.projectRootDirectory), projectRootDirectory: nullthrows(job.projectRootDirectory),
releaseChannel: job.releaseChannel,
updates: job.updates, updates: job.updates,
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined, secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
builderEnvironment: job.builderEnvironment, builderEnvironment: job.builderEnvironment,
@@ -32,10 +31,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
experimental: job.experimental, experimental: job.experimental,
mode: transformBuildMode(job.mode), mode: transformBuildMode(job.mode),
customBuildConfig: job.customBuildConfig, customBuildConfig: job.customBuildConfig,
environment: buildProfileEnvironmentToEnvironment(job.environment),
loggerLevel: job.loggerLevel
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
: undefined,
}; };
} }
+10 -31
View File
@@ -3,8 +3,9 @@ import {
BuildMode, BuildMode,
BuildTrigger, BuildTrigger,
Ios, Ios,
Job,
Platform, Platform,
sanitizeBuildJob, sanitizeJob,
} from '@expo/eas-build-job'; } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json'; import { BuildProfile } from '@expo/eas-json';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
@@ -13,7 +14,7 @@ import slash from 'slash';
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types'; import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated'; import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig'; import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils'; import { getUsername } from '../../project/projectUtils';
import { BuildContext } from '../context'; import { BuildContext } from '../context';
@@ -31,7 +32,7 @@ const cacheDefaults = {
export async function prepareJobAsync( export async function prepareJobAsync(
ctx: BuildContext<Platform.IOS>, ctx: BuildContext<Platform.IOS>,
jobData: JobData jobData: JobData
): Promise<Ios.Job> { ): Promise<Job> {
const username = getUsername(ctx.exp, ctx.user); const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile; const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory = const projectRootDirectory =
@@ -45,18 +46,9 @@ export async function prepareJobAsync(
} }
const maybeCustomBuildConfigPath = buildProfile.config const maybeCustomBuildConfigPath = buildProfile.config
? getCustomBuildConfigPathForJob(buildProfile.config) ? getCustomBuildConfigPath(buildProfile.config)
: undefined; : undefined;
let buildMode;
if (ctx.repack) {
buildMode = BuildMode.REPACK;
} else if (buildProfile.config) {
buildMode = BuildMode.CUSTOM;
} else {
buildMode = BuildMode.BUILD;
}
const job: Ios.Job = { const job: Ios.Job = {
type: ctx.workflow, type: ctx.workflow,
platform: Platform.IOS, platform: Platform.IOS,
@@ -65,17 +57,14 @@ export async function prepareJobAsync(
builderEnvironment: { builderEnvironment: {
image: buildProfile.image, image: buildProfile.image,
node: buildProfile.node, node: buildProfile.node,
corepack: buildProfile.corepack,
pnpm: buildProfile.pnpm, pnpm: buildProfile.pnpm,
bun: buildProfile.bun, bun: buildProfile.bun,
yarn: buildProfile.yarn, yarn: buildProfile.yarn,
bundler: buildProfile.bundler, bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods, cocoapods: buildProfile.cocoapods,
fastlane: buildProfile.fastlane, fastlane: buildProfile.fastlane,
env: { expoCli: buildProfile.expoCli,
...buildProfile.env, env: buildProfile.env,
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
},
}, },
cache: { cache: {
...cacheDefaults, ...cacheDefaults,
@@ -85,6 +74,7 @@ export async function prepareJobAsync(
secrets: { secrets: {
buildCredentials, buildCredentials,
}, },
releaseChannel: buildProfile.releaseChannel,
updates: { channel: buildProfile.channel }, updates: { channel: buildProfile.channel },
developmentClient: buildProfile.developmentClient, developmentClient: buildProfile.developmentClient,
simulator: buildProfile.simulator, simulator: buildProfile.simulator,
@@ -92,7 +82,6 @@ export async function prepareJobAsync(
buildConfiguration: buildProfile.buildConfiguration, buildConfiguration: buildProfile.buildConfiguration,
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath, applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
buildArtifactPaths: buildProfile.buildArtifactPaths, buildArtifactPaths: buildProfile.buildArtifactPaths,
environment: ctx.buildProfile.environment,
username, username,
...(ctx.ios.buildNumberOverride && { ...(ctx.ios.buildNumberOverride && {
version: { version: {
@@ -102,25 +91,15 @@ export async function prepareJobAsync(
experimental: { experimental: {
prebuildCommand: buildProfile.prebuildCommand, prebuildCommand: buildProfile.prebuildCommand,
}, },
mode: buildMode, mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
triggeredBy: BuildTrigger.EAS_CLI, triggeredBy: BuildTrigger.EAS_CLI,
...(maybeCustomBuildConfigPath && { ...(maybeCustomBuildConfigPath && {
customBuildConfig: { customBuildConfig: {
path: maybeCustomBuildConfigPath, path: maybeCustomBuildConfigPath,
}, },
}), }),
...(ctx.repack && {
customBuildConfig: {
path: '__eas/repack.yml',
},
}),
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 { export function prepareCredentialsToResign(credentials: IosCredentials): IosJobSecretsInput {
@@ -1,35 +1,39 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config';
import { Env, Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { IosVersionAutoIncrement } from '@expo/eas-json'; import { IosVersionAutoIncrement } from '@expo/eas-json';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version'; import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { Target } from '../../credentials/ios/types'; import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils'; import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow'; import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule'; import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { Client } from '../../vcs/vcs'; import { Client } from '../../vcs/vcs';
export async function syncProjectConfigurationAsync({ export async function syncProjectConfigurationAsync(
graphqlClient: ExpoGraphqlClient,
{
projectDir, projectDir,
exp, exp,
targets, targets,
localAutoIncrement, localAutoIncrement,
projectId,
vcsClient, vcsClient,
env,
}: { }: {
projectDir: string; projectDir: string;
exp: ExpoConfig; exp: ExpoConfig;
targets: Target[]; targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement; localAutoIncrement?: IosVersionAutoIncrement;
projectId: string;
vcsClient: Client; vcsClient: Client;
env: Env | undefined; }
}): Promise<void> { ): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient); const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false); const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) { if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) { if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env }); await syncUpdatesConfigurationAsync(graphqlClient, vcsClient, projectDir, exp, projectId);
} }
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets }); await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else { } else {
+11 -38
View File
@@ -11,7 +11,7 @@ import { Target } from '../../credentials/ios/types';
import { AppPlatform } from '../../graphql/generated'; import { AppPlatform } from '../../graphql/generated';
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation'; import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery'; import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
import Log, { learnMore } from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { findApplicationTarget } from '../../project/ios/target'; import { findApplicationTarget } from '../../project/ios/target';
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions'; import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
@@ -23,7 +23,7 @@ import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson'; import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version'; import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/; const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
export enum BumpStrategy { export enum BumpStrategy {
APP_VERSION, APP_VERSION,
@@ -100,27 +100,11 @@ export async function bumpVersionInAppJsonAsync({
} }
} }
function validateShortVersion({ function validateShortVersion(shortVersion: string | undefined): void {
shortVersion,
workflow,
}: {
shortVersion: string | undefined;
workflow: Workflow;
}): void {
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) { if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
if (workflow === Workflow.MANAGED) { throw new Error(
Log.warn( `CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: ${shortVersion}`
`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'
)}`
);
}
} }
} }
@@ -138,10 +122,10 @@ export async function readShortVersionAsync(
infoPlist.CFBundleShortVersionString && infoPlist.CFBundleShortVersionString &&
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings); evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
validateShortVersion({ shortVersion, workflow }); validateShortVersion(shortVersion);
return shortVersion; return shortVersion;
} else { } else {
validateShortVersion({ shortVersion: exp.version, workflow }); validateShortVersion(exp.version);
return exp.version; return exp.version;
} }
} }
@@ -330,18 +314,14 @@ export async function resolveRemoteBuildNumberAsync(
vcsClient vcsClient
); );
let currentBuildVersion: string; let currentBuildVersion: string;
let shouldInitializeBuildNumber = false;
if (remoteVersions?.buildVersion) { if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion; currentBuildVersion = remoteVersions.buildVersion;
} else { } else {
if (localBuildNumber) { if (localBuildNumber) {
Log.log( Log.warn(
chalk.green(
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.' 'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
)
); );
currentBuildVersion = localBuildNumber; currentBuildVersion = localBuildNumber;
shouldInitializeBuildNumber = localBuildNumber === '1';
} else { } else {
Log.error( 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.` `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 +331,7 @@ export async function resolveRemoteBuildNumberAsync(
} }
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) { if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
return currentBuildVersion; return currentBuildVersion;
} else if ( } else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
shouldInitializeBuildNumber
) {
const spinner = ora( const spinner = ora(
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.` `Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
).start(); ).start();
@@ -365,9 +342,7 @@ export async function resolveRemoteBuildNumberAsync(
applicationIdentifier: applicationTarget.bundleIdentifier, applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0', storeVersion: localShortVersion ?? '1.0.0',
buildVersion: currentBuildVersion, buildVersion: currentBuildVersion,
runtimeVersion: runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
undefined,
}); });
spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`); spinner.succeed(`Initialized buildNumber with ${chalk.bold(currentBuildVersion)}.`);
} catch (err) { } catch (err) {
@@ -389,9 +364,7 @@ export async function resolveRemoteBuildNumberAsync(
applicationIdentifier: applicationTarget.bundleIdentifier, applicationIdentifier: applicationTarget.bundleIdentifier,
storeVersion: localShortVersion ?? '1.0.0', storeVersion: localShortVersion ?? '1.0.0',
buildVersion: nextBuildVersion, buildVersion: nextBuildVersion,
runtimeVersion: runtimeVersion: Updates.getRuntimeVersionNullable(exp, Platform.IOS) ?? undefined,
(await Updates.getRuntimeVersionNullableAsync(projectDir, exp, Platform.IOS)) ??
undefined,
}); });
spinner.succeed( spinner.succeed(
`Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold( `Incremented buildNumber from ${chalk.bold(currentBuildVersion)} to ${chalk.bold(
+6 -16
View File
@@ -3,11 +3,10 @@ import spawnAsync from '@expo/spawn-async';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import semver from 'semver'; import semver from 'semver';
import Log from '../log';
import { ora } from '../ora'; import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin'; const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.171'; const PLUGIN_PACKAGE_VERSION = '1.0.56';
export enum LocalBuildMode { export enum LocalBuildMode {
/** /**
@@ -40,8 +39,7 @@ export interface LocalBuildOptions {
export async function runLocalBuildAsync( export async function runLocalBuildAsync(
job: Job, job: Job,
metadata: Metadata, metadata: Metadata,
options: LocalBuildOptions, options: LocalBuildOptions
env: Record<string, string>
): Promise<void> { ): Promise<void> {
const { command, args } = await getCommandAndArgsAsync(job, metadata); const { command, args } = await getCommandAndArgsAsync(job, metadata);
let spinner; let spinner;
@@ -56,8 +54,9 @@ export async function runLocalBuildAsync(
}; };
process.on('SIGINT', interruptHandler); process.on('SIGINT', interruptHandler);
try { try {
const mergedEnv = { const spawnPromise = spawnAsync(command, args, {
...env, stdio: options.verbose ? 'inherit' : 'pipe',
env: {
...process.env, ...process.env,
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR, EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
...(options.skipCleanup || options.skipNativeBuild ...(options.skipCleanup || options.skipNativeBuild
@@ -66,16 +65,7 @@ export async function runLocalBuildAsync(
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}), ...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}), ...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}), ...(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,
}); });
childProcess = spawnPromise.child; childProcess = spawnPromise.child;
await spawnPromise; await spawnPromise;
+39 -20
View File
@@ -1,4 +1,5 @@
import { FingerprintSource, Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job'; import { Updates } from '@expo/config-plugins';
import { Metadata, Platform, sanitizeMetadata } from '@expo/eas-build-job';
import { IosEnterpriseProvisioning } from '@expo/eas-json'; import { IosEnterpriseProvisioning } from '@expo/eas-json';
import fs from 'fs-extra'; import fs from 'fs-extra';
import resolveFrom from 'resolve-from'; import resolveFrom from 'resolve-from';
@@ -7,23 +8,26 @@ import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from '.
import { BuildContext } from './context'; import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version'; import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
import { LocalBuildMode } from './local'; import { LocalBuildMode } from './local';
import { BuildDistributionType } from './types';
import Log from '../log'; import Log from '../log';
import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils'; import { getUsername, isExpoUpdatesInstalled } from '../project/projectUtils';
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule'; import {
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule'; 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 { easCliVersion } from '../utils/easCli';
export async function collectMetadataAsync<T extends Platform>( export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>, ctx: BuildContext<T>
runtimeAndFingerprintMetadata: {
runtimeVersion?: string | undefined;
fingerprintHash?: string | undefined;
fingerprintSource?: FingerprintSource | undefined;
}
): Promise<Metadata> { ): Promise<Metadata> {
const channelObject = await resolveChannelAsync(ctx); const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE; const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
? 'simulator'
: ctx.buildProfile.distribution) ?? 'store';
const metadata: Metadata = { const metadata: Metadata = {
trackingContext: ctx.analyticsEventProperties, trackingContext: ctx.analyticsEventProperties,
...(await maybeResolveVersionsAsync(ctx)), ...(await maybeResolveVersionsAsync(ctx)),
@@ -31,11 +35,9 @@ export async function collectMetadataAsync<T extends Platform>(
workflow: ctx.workflow, workflow: ctx.workflow,
credentialsSource: ctx.buildProfile.credentialsSource, credentialsSource: ctx.buildProfile.credentialsSource,
sdkVersion: ctx.exp.sdkVersion, sdkVersion: ctx.exp.sdkVersion,
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion, runtimeVersion: Updates.getRuntimeVersionNullable(ctx.exp, ctx.platform) ?? undefined,
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
fingerprintSource: runtimeAndFingerprintMetadata?.fingerprintSource,
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir), reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
...channelObject, ...channelOrReleaseChannel,
distribution, distribution,
appName: ctx.exp.name, appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx), appIdentifier: resolveAppIdentifier(ctx),
@@ -62,7 +64,6 @@ export async function collectMetadataAsync<T extends Platform>(
requiredPackageManager: ctx.requiredPackageManager ?? undefined, requiredPackageManager: ctx.requiredPackageManager ?? undefined,
selectedImage: ctx.buildProfile.image, selectedImage: ctx.buildProfile.image,
customNodeVersion: ctx.buildProfile.node, customNodeVersion: ctx.buildProfile.node,
simulator: 'simulator' in ctx.buildProfile && ctx.buildProfile.simulator,
}; };
return sanitizeMetadata(metadata); return sanitizeMetadata(metadata);
} }
@@ -113,21 +114,39 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
} }
} }
async function resolveChannelAsync<T extends Platform>( async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T> ctx: BuildContext<T>
): Promise<{ channel: string } | null> { ): Promise<{ channel: string } | { releaseChannel: string } | null> {
if (!isExpoUpdatesInstalled(ctx.projectDir)) { if (!isExpoUpdatesInstalled(ctx.projectDir)) {
return null; return null;
} }
if (ctx.buildProfile.channel) { if (ctx.buildProfile.channel) {
return { channel: ctx.buildProfile.channel }; return { channel: ctx.buildProfile.channel };
} }
if (ctx.buildProfile.releaseChannel) {
return { releaseChannel: ctx.buildProfile.releaseChannel };
}
const channel = await getNativeChannelAsync(ctx); const channel = await getNativeChannelAsync(ctx);
if (channel) { if (channel) {
return { channel }; return { channel };
} }
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
return { releaseChannel };
}
return null; async function getNativeReleaseChannelAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<string> {
switch (ctx.platform) {
case Platform.ANDROID: {
return (await readAndroidReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
case Platform.IOS: {
return (await readIosReleaseChannelSafelyAsync(ctx.projectDir)) ?? 'default';
}
default:
return 'default';
}
} }
async function getNativeChannelAsync<T extends Platform>( async function getNativeChannelAsync<T extends Platform>(
+2 -3
View File
@@ -50,9 +50,8 @@ export async function listAndRenderBuildsOnAppAsync(
}), }),
promptOptions: { promptOptions: {
title: 'Load more builds?', title: 'Load more builds?',
renderListItems: builds => { renderListItems: builds =>
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }); renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
},
}, },
}); });
} }
+47 -168
View File
@@ -1,17 +1,16 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config-types';
import { Env, Platform, Workflow } from '@expo/eas-build-job'; import { Platform, Workflow } from '@expo/eas-build-job';
import { import {
AppVersionSource, AppVersionSource,
BuildProfile,
EasJson, EasJson,
EasJsonAccessor, EasJsonAccessor,
EasJsonUtils, EasJsonUtils,
ResourceClass, ResourceClass,
SubmitProfile, SubmitProfile,
} from '@expo/eas-json'; } from '@expo/eas-json';
import { LoggerLevel } from '@expo/logger';
import assert from 'assert'; import assert from 'assert';
import chalk from 'chalk'; import chalk from 'chalk';
import { pathExists } from 'fs-extra';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import { prepareAndroidBuildAsync } from './android/build'; import { prepareAndroidBuildAsync } from './android/build';
@@ -19,9 +18,8 @@ import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './
import { ensureProjectConfiguredAsync } from './configure'; import { ensureProjectConfiguredAsync } from './configure';
import { BuildContext } from './context'; import { BuildContext } from './context';
import { createBuildContextAsync } from './createContext'; import { createBuildContextAsync } from './createContext';
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
import { prepareIosBuildAsync } from './ios/build'; import { prepareIosBuildAsync } from './ios/build';
import { LocalBuildMode, LocalBuildOptions } from './local'; import { LocalBuildOptions } from './local';
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient'; import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
import { printBuildResults, printLogsUrls } from './utils/printBuildInfo'; import { printBuildResults, printLogsUrls } from './utils/printBuildInfo';
import { ensureRepoIsCleanAsync } from './utils/repository'; import { ensureRepoIsCleanAsync } from './utils/repository';
@@ -58,12 +56,11 @@ import {
validateAppVersionRuntimePolicySupportAsync, validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils'; } from '../project/projectUtils';
import { import {
ensureAppVersionSourceIsSetAsync,
validateAppConfigForRemoteVersionSource, validateAppConfigForRemoteVersionSource,
validateBuildProfileVersionSettingsAsync, validateBuildProfileVersionSettings,
} from '../project/remoteVersionSource'; } from '../project/remoteVersionSource';
import { confirmAsync } from '../prompts'; import { confirmAsync } from '../prompts';
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run'; import { runAsync } from '../run/run';
import { isRunnableOnSimulatorOrEmulator } from '../run/utils'; import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
import { createSubmissionContextAsync } from '../submit/context'; import { createSubmissionContextAsync } from '../submit/context';
import { import {
@@ -73,6 +70,7 @@ import {
} from '../submit/submit'; } from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls'; import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure'; import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User'; import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download'; import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter'; import { truthy } from '../utils/expodash/filter';
@@ -95,36 +93,17 @@ export interface BuildFlags {
localBuildOptions: LocalBuildOptions; localBuildOptions: LocalBuildOptions;
resourceClass?: ResourceClass; resourceClass?: ResourceClass;
message?: string; message?: string;
buildLoggerLevel?: LoggerLevel;
freezeCredentials: boolean;
repack: boolean;
isVerboseLoggingEnabled?: boolean;
} }
export async function runBuildAndSubmitAsync({ export async function runBuildAndSubmitAsync(
graphqlClient, graphqlClient: ExpoGraphqlClient,
analytics, analytics: Analytics,
vcsClient, vcsClient: Client,
projectDir, projectDir: string,
flags, flags: BuildFlags,
actor, actor: Actor,
getDynamicPrivateProjectConfigAsync, getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
downloadSimBuildAutoConfirm, ): Promise<void> {
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 vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive); await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
@@ -145,32 +124,13 @@ export async function runBuildAndSubmitAsync({
profileName: flags.profile ?? undefined, profileName: flags.profile ?? undefined,
projectDir, projectDir,
}); });
Log.log(
for (const buildProfile of buildProfiles) { `Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) { buildProfiles[0].profile.env
Log.warn( ? Object.keys(buildProfiles[0].profile.env).join(', ')
`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/` : 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
); );
} 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'
)}`
);
}
}
await ensureExpoDevClientInstalledForDevClientBuildsAsync({ await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir, projectDir,
@@ -182,12 +142,7 @@ export async function runBuildAndSubmitAsync({
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } = const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
{}; {};
for (const buildProfile of buildProfiles) { for (const buildProfile of buildProfiles) {
await validateBuildProfileVersionSettingsAsync( validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
buildProfile,
easJsonCliConfig,
projectDir,
flags
);
const maybeMetadata = await validateCustomBuildConfigAsync({ const maybeMetadata = await validateCustomBuildConfigAsync({
projectDir, projectDir,
profile: buildProfile.profile, profile: buildProfile.profile,
@@ -206,17 +161,6 @@ export async function runBuildAndSubmitAsync({
for (const buildProfile of buildProfiles) { for (const buildProfile of buildProfiles) {
const platform = toAppPlatform(buildProfile.platform); 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({ const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
projectDir, projectDir,
flags, flags,
@@ -229,7 +173,6 @@ export async function runBuildAndSubmitAsync({
vcsClient, vcsClient,
getDynamicPrivateProjectConfigAsync, getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform], customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
env,
}); });
if (maybeBuild) { if (maybeBuild) {
startedBuilds.push({ build: maybeBuild, buildProfile }); startedBuilds.push({ build: maybeBuild, buildProfile });
@@ -237,25 +180,13 @@ export async function runBuildAndSubmitAsync({
buildCtxByPlatform[platform] = buildCtx; buildCtxByPlatform[platform] = buildCtx;
} }
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) { if (flags.localBuildOptions.localBuildMode) {
return { return;
buildIds: startedBuilds.map(({ build }) => build.id),
};
} }
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(); Log.newLine();
printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build)); printLogsUrls(startedBuilds.map(startedBuild => startedBuild.build));
Log.newLine(); Log.newLine();
}
const submissions: SubmissionFragment[] = []; const submissions: SubmissionFragment[] = [];
if (flags.autoSubmit) { if (flags.autoSubmit) {
@@ -277,9 +208,9 @@ export async function runBuildAndSubmitAsync({
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]), buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
moreBuilds: startedBuilds.length > 1, moreBuilds: startedBuilds.length > 1,
projectDir, projectDir,
buildProfile: startedBuild.buildProfile.profile,
submitProfile, submitProfile,
nonInteractive: flags.nonInteractive, nonInteractive: flags.nonInteractive,
selectedSubmitProfileName: flags.submitProfile,
}); });
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync( startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
graphqlClient, graphqlClient,
@@ -288,26 +219,16 @@ export async function runBuildAndSubmitAsync({
submissions.push(submission); submissions.push(submission);
} }
if (!flags.localBuildOptions.localBuildMode) {
Log.newLine(); Log.newLine();
printSubmissionDetailsUrls(submissions); printSubmissionDetailsUrls(submissions);
Log.newLine(); Log.newLine();
} }
}
if (flags.localBuildOptions.localBuildMode) {
return {
buildIds: startedBuilds.map(({ build }) => build.id),
};
}
if (!flags.wait) { if (!flags.wait) {
if (flags.json) { if (flags.json) {
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build)); printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
} }
return { return;
buildIds: startedBuilds.map(({ build }) => build.id),
};
} }
const { accountName } = Object.values(buildCtxByPlatform)[0]; const { accountName } = Object.values(buildCtxByPlatform)[0];
@@ -325,7 +246,7 @@ export async function runBuildAndSubmitAsync({
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status) [BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
); );
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm); await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) { if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
if (flags.json) { if (flags.json) {
@@ -348,10 +269,6 @@ export async function runBuildAndSubmitAsync({
} }
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions); exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
} }
return {
buildIds: startedBuilds.map(({ build }) => build.id),
buildProfiles,
};
} }
async function prepareAndStartBuildAsync({ async function prepareAndStartBuildAsync({
@@ -366,7 +283,6 @@ async function prepareAndStartBuildAsync({
vcsClient, vcsClient,
getDynamicPrivateProjectConfigAsync, getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata, customBuildConfigMetadata,
env,
}: { }: {
projectDir: string; projectDir: string;
flags: BuildFlags; flags: BuildFlags;
@@ -379,7 +295,6 @@ async function prepareAndStartBuildAsync({
vcsClient: Client; vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn; getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata; customBuildConfigMetadata?: CustomBuildConfigMetadata;
env: Env;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> { }): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
const buildCtx = await createBuildContextAsync({ const buildCtx = await createBuildContextAsync({
buildProfileName: buildProfile.profileName, buildProfileName: buildProfile.profileName,
@@ -399,11 +314,6 @@ async function prepareAndStartBuildAsync({
vcsClient, vcsClient,
getDynamicPrivateProjectConfigAsync, getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata, customBuildConfigMetadata,
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
freezeCredentials: flags.freezeCredentials,
repack: flags.repack,
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
env,
}); });
if (moreBuilds) { if (moreBuilds) {
@@ -416,8 +326,15 @@ async function prepareAndStartBuildAsync({
); );
} }
await validateBuildProfileConfigMatchesProjectConfigAsync(
buildCtx.exp,
buildProfile,
buildCtx.projectId,
flags.nonInteractive
);
if (buildProfile.profile.channel) { if (buildProfile.profile.channel) {
await validateExpoUpdatesInstalledAsProjectDependencyAsync({ await validateExpoUpdatesInstalledAsProjectDependencyAsync({
graphqlClient,
exp: buildCtx.exp, exp: buildCtx.exp,
projectId: buildCtx.projectId, projectId: buildCtx.projectId,
projectDir, projectDir,
@@ -425,7 +342,6 @@ async function prepareAndStartBuildAsync({
sdkVersion: buildCtx.exp.sdkVersion, sdkVersion: buildCtx.exp.sdkVersion,
nonInteractive: flags.nonInteractive, nonInteractive: flags.nonInteractive,
buildProfile, buildProfile,
env: buildProfile.profile.env,
}); });
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) { if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, { const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
@@ -442,25 +358,6 @@ async function prepareAndStartBuildAsync({
} }
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp); 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) { if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform); validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
} }
@@ -497,16 +394,16 @@ async function prepareAndStartSubmissionAsync({
buildCtx, buildCtx,
moreBuilds, moreBuilds,
projectDir, projectDir,
buildProfile,
submitProfile, submitProfile,
selectedSubmitProfileName,
nonInteractive, nonInteractive,
}: { }: {
build: BuildFragment; build: BuildFragment;
buildCtx: BuildContext<Platform>; buildCtx: BuildContext<Platform>;
moreBuilds: boolean; moreBuilds: boolean;
projectDir: string; projectDir: string;
buildProfile: BuildProfile;
submitProfile: SubmitProfile; submitProfile: SubmitProfile;
selectedSubmitProfileName?: string;
nonInteractive: boolean; nonInteractive: boolean;
}): Promise<SubmissionFragment> { }): Promise<SubmissionFragment> {
const platform = toPlatform(build.platform); const platform = toPlatform(build.platform);
@@ -516,7 +413,7 @@ async function prepareAndStartSubmissionAsync({
profile: submitProfile, profile: submitProfile,
archiveFlags: { id: build.id }, archiveFlags: { id: build.id },
nonInteractive, nonInteractive,
env: buildCtx.env, env: buildProfile.env,
credentialsCtx: buildCtx.credentialsCtx, credentialsCtx: buildCtx.credentialsCtx,
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier, applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
actor: buildCtx.user, actor: buildCtx.user,
@@ -525,8 +422,6 @@ async function prepareAndStartSubmissionAsync({
projectId: buildCtx.projectId, projectId: buildCtx.projectId,
exp: buildCtx.exp, exp: buildCtx.exp,
vcsClient: buildCtx.vcsClient, vcsClient: buildCtx.vcsClient,
isVerboseFastlaneEnabled: false,
specifiedProfile: selectedSubmitProfileName,
}); });
if (moreBuilds) { if (moreBuilds) {
@@ -550,29 +445,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); 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( const buildPath = await downloadAndMaybeExtractAppAsync(
build.artifacts.applicationArchiveUrl, build.artifacts.applicationArchiveUrl,
build.platform, build.platform
cachedAppPath
); );
await runAsync(buildPath, build.platform); await runAsync(buildPath, build.platform);
} }
async function maybeDownloadAndRunSimulatorBuildsAsync( async function maybeDownloadAndRunSimulatorBuildsAsync(
builds: MaybeBuildFragment[], builds: MaybeBuildFragment[],
flags: BuildFlags, flags: BuildFlags
autoConfirm?: boolean
): Promise<void> { ): Promise<void> {
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator); const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
@@ -580,15 +464,11 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
for (const simBuild of simBuilds) { for (const simBuild of simBuilds) {
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') { if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
Log.newLine(); Log.newLine();
const confirm = const confirm = await confirmAsync({
autoConfirm ??
(await confirmAsync({
message: `Install and run the ${ message: `Install and run the ${
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS' simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
} build on ${ } build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator' });
}?`,
}));
if (confirm) { if (confirm) {
await downloadAndRunAsync(simBuild); await downloadAndRunAsync(simBuild);
} }
@@ -599,14 +479,15 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
async function validateExpoUpdatesInstalledAsProjectDependencyAsync({ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp, exp,
graphqlClient,
projectId, projectId,
projectDir, projectDir,
vcsClient, vcsClient,
buildProfile, buildProfile,
nonInteractive, nonInteractive,
sdkVersion, sdkVersion,
env,
}: { }: {
graphqlClient: ExpoGraphqlClient;
exp: ExpoConfig; exp: ExpoConfig;
projectId: string; projectId: string;
projectDir: string; projectDir: string;
@@ -614,7 +495,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
buildProfile: ProfileData<'build'>; buildProfile: ProfileData<'build'>;
nonInteractive: boolean; nonInteractive: boolean;
sdkVersion?: string; sdkVersion?: string;
env: Env | undefined;
}): Promise<void> { }): Promise<void> {
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) { if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
return; return;
@@ -636,13 +516,12 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
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 and configure EAS Update now?`,
}); });
if (installExpoUpdates) { if (installExpoUpdates) {
await ensureEASUpdateIsConfiguredAsync({ await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp, exp,
projectId, projectId,
projectDir, projectDir,
platform: RequestedPlatform.All, platform: RequestedPlatform.All,
vcsClient, vcsClient,
env,
}); });
Log.withTick('Installed expo-updates and configured EAS Update.'); Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.'); throw new Error('Command must be re-run to pick up new updates configuration.');
-24
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 { export enum BuildStatus {
NEW = 'new', NEW = 'new',
IN_QUEUE = 'in-queue', IN_QUEUE = 'in-queue',
@@ -17,23 +11,5 @@ export enum BuildStatus {
export enum BuildDistributionType { export enum BuildDistributionType {
STORE = 'store', STORE = 'store',
INTERNAL = 'internal', INTERNAL = 'internal',
/** @deprecated Use simulator flag instead */
SIMULATOR = 'simulator', SIMULATOR = 'simulator',
} }
export 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;
repack: boolean;
}
@@ -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];
}
@@ -39,10 +39,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
} }
}, },
}, },
{
label: 'Profile',
value: build.buildProfile,
},
{ {
label: 'Message', label: 'Message',
value: build.message, value: build.message,
@@ -55,6 +51,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
label: 'Enterprise Provisioning', label: 'Enterprise Provisioning',
value: build.iosEnterpriseProvisioning?.toLowerCase(), value: build.iosEnterpriseProvisioning?.toLowerCase(),
}, },
{
label: 'Release Channel',
value: build.releaseChannel,
},
{ {
label: 'Channel', label: 'Channel',
value: build.channel, value: build.channel,
@@ -62,9 +62,7 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
assert(build, 'Build should be defined'); assert(build, 'Build should be defined');
printBuildResult(build); printBuildResult(build);
} else { } else {
(builds.filter(i => i) as BuildFragment[]).forEach(build => { (builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(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 // to the build details page and let people press the button to download there
const qrcodeUrl = const qrcodeUrl =
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl; build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code => { qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
Log.log(`${indentString(code, 2)}\n`); Log.log(`${indentString(code, 2)}\n`)
}); );
Log.log( Log.log(
`${appPlatformEmojis[build.platform]} Open this link on your ${ `${appPlatformEmojis[build.platform]} Open this link on your ${
appPlatformDisplayNames[build.platform] appPlatformDisplayNames[build.platform]
+5 -68
View File
@@ -4,7 +4,7 @@ import path from 'path';
import tar from 'tar'; import tar from 'tar';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import Log, { learnMore } from '../../log'; import Log from '../../log';
import { ora } from '../../ora'; import { ora } from '../../ora';
import { confirmAsync, promptAsync } from '../../prompts'; import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files'; import { formatBytes } from '../../utils/files';
@@ -12,10 +12,7 @@ import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer'; import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { Client } from '../../vcs/vcs'; import { Client } from '../../vcs/vcs';
export async function maybeBailOnRepoStatusAsync( export async function maybeBailOnRepoStatusAsync(vcsClient: Client): Promise<void> {
vcsClient: Client,
nonInteractive: boolean
): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) { if (!(await vcsClient.isCommitRequiredAsync())) {
return; return;
} }
@@ -31,11 +28,6 @@ export async function maybeBailOnRepoStatusAsync(
}); });
if (!answer) { if (!answer) {
if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
}
throw new Error('Commit all changes. Aborting...'); throw new Error('Commit all changes. Aborting...');
} }
} }
@@ -55,9 +47,6 @@ export async function ensureRepoIsCleanAsync(
)}.` )}.`
); );
if (nonInteractive) { if (nonInteractive) {
Log.log('The following files need to be committed:');
await vcsClient.showChangedFilesAsync();
throw new Error('Commit all changes. Aborting...'); throw new Error('Commit all changes. Aborting...');
} }
const answer = await confirmAsync({ const answer = await confirmAsync({
@@ -94,33 +83,9 @@ export async function commitPromptAsync(
}); });
} }
export type LocalFile = { export async function makeProjectTarballAsync(
path: string; vcsClient: Client
size: number; ): Promise<{ 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> {
const spinner = ora('Compressing project files'); const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory()); await fs.mkdirp(getTmpDirectory());
@@ -169,34 +134,6 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
return { size, path: tarPath }; 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 { enum ShouldCommitChanges {
Yes, Yes,
ShowDiffFirst, ShowDiffFirst,
-31
View File
@@ -50,34 +50,3 @@ export function getUpdateGroupUrl(
getExpoWebsiteBaseUrl() getExpoWebsiteBaseUrl()
).toString(); ).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();
}
+8 -12
View File
@@ -9,9 +9,9 @@ import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image'; import { isPNGAsync } from '../utils/image';
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void { export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
if (ctx.env?.NODE_ENV === 'production') { if (ctx.buildProfile.env?.NODE_ENV === 'production') {
Log.warn( 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(); Log.newLine();
} }
@@ -27,28 +27,24 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) { if (!googleServicesFilePath) {
return; 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 ctx.vcsClient.getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath); const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if ( if (
(await fs.pathExists(absGoogleServicesFilePath)) && (await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) || (!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync( (await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
path.relative(rootDir, absGoogleServicesFilePath)
))) &&
!googleServicesEnvVar
) { ) {
Log.warn( Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.` `File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
); );
Log.warn( 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( `Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables' '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(); Log.newLine();
} }
} }
@@ -17,9 +17,7 @@ import {
describe(assertVersion, () => { describe(assertVersion, () => {
it('throws if the branch mapping is not the correct version', () => { it('throws if the branch mapping is not the correct version', () => {
expect(() => { expect(() => assertVersion(testChannelBasicInfo, 5)).toThrowError(BranchMappingValidationError);
assertVersion(testChannelBasicInfo, 5);
}).toThrowError(BranchMappingValidationError);
}); });
it('asserts the correct version', () => { it('asserts the correct version', () => {
assertVersion(testChannelBasicInfo, 0); assertVersion(testChannelBasicInfo, 0);
@@ -128,7 +128,6 @@ export const testChannelObject: UpdateChannelObject = {
branchMapping: 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}', '{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
updateBranches: [testUpdateBranch1, testUpdateBranch2], updateBranches: [testUpdateBranch1, testUpdateBranch2],
isPaused: false,
__typename: 'UpdateChannel', __typename: 'UpdateChannel',
}; };
@@ -28,7 +28,7 @@ import {
*/ */
export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFragment | null> { export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFragment | null> {
constructor( constructor(
private readonly options: { private options: {
filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean; filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean;
printedType?: string; printedType?: string;
pageSize?: number; pageSize?: number;
@@ -91,8 +91,8 @@ export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFrag
queryParams: QueryParams queryParams: QueryParams
): Promise<Connection<UpdateChannelBasicInfoFragment>> => ): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
filterPredicate filterPredicate
? await this.filterQueryAsync(ctx, queryParams, filterPredicate) ? this.filterQueryAsync(ctx, queryParams, filterPredicate)
: await this.queryAsync(ctx, queryParams); : this.queryAsync(ctx, queryParams);
const getTitleAsync = async (channelInfo: UpdateChannelBasicInfoFragment): Promise<string> => const getTitleAsync = async (channelInfo: UpdateChannelBasicInfoFragment): Promise<string> =>
channelInfo.name; channelInfo.name;
+27 -38
View File
@@ -19,7 +19,7 @@ import {
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery'; import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery'; import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo'; import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log, { learnMore } from '../log'; import Log from '../log';
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils'; import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
import formatFields from '../utils/formatFields'; import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json'; import { printJsonOnlyOutput } from '../utils/json';
@@ -89,9 +89,7 @@ export async function listAndRenderChannelsOnAppAsync(
queryChannelsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }), queryChannelsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }),
promptOptions: { promptOptions: {
title: 'Load more channels?', title: 'Load more channels?',
renderListItems: channels => { renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
renderPageOfChannels(channels, paginatedQueryOptions);
},
}, },
}); });
} }
@@ -110,11 +108,7 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
} }
): Promise<void> { ): Promise<void> {
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName }); const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
renderChannelHeaderContent({ renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
channelName: channel.name,
channelId: channel.id,
isPaused: channel.isPaused,
});
if (paginatedQueryOptions.nonInteractive) { if (paginatedQueryOptions.nonInteractive) {
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, { const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
@@ -137,9 +131,8 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
}), }),
promptOptions: { promptOptions: {
title: 'Load more channels?', title: 'Load more channels?',
renderListItems: branches => { renderListItems: branches =>
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions); renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
},
}, },
}); });
} }
@@ -171,11 +164,7 @@ function renderPageOfChannels(
printJsonOnlyOutput({ currentPage }); printJsonOnlyOutput({ currentPage });
} else { } else {
for (const channel of currentPage) { for (const channel of currentPage) {
renderChannelHeaderContent({ renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
channelName: channel.name,
channelId: channel.id,
isPaused: channel.isPaused,
});
Log.addNewLineIfNone(); Log.addNewLineIfNone();
logChannelDetails(channel); logChannelDetails(channel);
@@ -204,11 +193,9 @@ function renderPageOfBranchesOnChannel(
function renderChannelHeaderContent({ function renderChannelHeaderContent({
channelName, channelName,
channelId, channelId,
isPaused,
}: { }: {
channelName: string; channelName: string;
channelId: string; channelId: string;
isPaused: boolean;
}): void { }): void {
Log.addNewLineIfNone(); Log.addNewLineIfNone();
Log.log(chalk.bold('Channel:')); Log.log(chalk.bold('Channel:'));
@@ -216,7 +203,6 @@ function renderChannelHeaderContent({
formatFields([ formatFields([
{ label: 'Name', value: channelName }, { label: 'Name', value: channelName },
{ label: 'ID', value: channelId }, { label: 'ID', value: channelId },
{ label: 'Status', value: isPaused ? 'Paused' : 'Active' },
]) ])
); );
Log.addNewLineIfNone(); Log.addNewLineIfNone();
@@ -289,7 +275,7 @@ export async function doesChannelExistAsync(
{ appId, channelName }: { appId: string; channelName: string } { appId, channelName }: { appId: string; channelName: string }
): Promise<boolean> { ): Promise<boolean> {
try { try {
await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, { await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId, appId,
channelName, channelName,
}); });
@@ -303,6 +289,7 @@ export async function doesChannelExistAsync(
} }
/** /**
*
* Creates a channel and links it to a branch with the same name. * Creates a channel and links it to a branch with the same name.
* *
* @param appId the app ID, also known as the project ID * @param appId the app ID, also known as the project ID
@@ -318,7 +305,7 @@ export async function createAndLinkChannelAsync(
}: { appId: string; channelName: string; shouldPrintJson?: boolean } }: { appId: string; channelName: string; shouldPrintJson?: boolean }
): Promise<ChannelBasicInfo> { ): Promise<ChannelBasicInfo> {
let branchId: string; let branchId: string;
let hasCreatedBranch = false; let branchMessage: string;
try { try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, { const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
@@ -326,6 +313,7 @@ export async function createAndLinkChannelAsync(
name: channelName, name: channelName,
}); });
branchId = branch.id; branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) { } catch (error) {
if (error instanceof BranchNotFoundError) { if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, { const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
@@ -333,7 +321,7 @@ export async function createAndLinkChannelAsync(
name: channelName, name: channelName,
}); });
branchId = newBranch.id; branchId = newBranch.id;
hasCreatedBranch = true; branchMessage = `We also went ahead and made a branch with the same name`;
} else { } else {
throw error; throw error;
} }
@@ -356,25 +344,26 @@ export async function createAndLinkChannelAsync(
if (shouldPrintJson) { if (shouldPrintJson) {
printJsonOnlyOutput(newChannel); printJsonOnlyOutput(newChannel);
} else { } else {
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, appId);
const learnMoreLink = learnMore('https://docs.expo.dev/eas-update/eas-cli/');
Log.addNewLineIfNone(); Log.addNewLineIfNone();
if (hasCreatedBranch) {
Log.withTick( Log.withTick(
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} and branch ${chalk.bold( `Created a new channel on project ${chalk.bold(
`"${newChannel.name}"` await getDisplayNameForProjectIdAsync(graphqlClient, appId)
)} on ${chalk.bold(projectDisplayName)} project. ${learnMoreLink}` )}`
); );
} else { Log.log(
Log.withTick( formatFields([
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} on ${chalk.bold( { label: 'Name', value: newChannel.name },
projectDisplayName { label: 'ID', value: newChannel.id },
)} project and connected it with existing ${chalk.bold( ])
`"${newChannel.name}"` );
)} branch. ${learnMoreLink}` Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
); );
}
} }
return newChannel; return newChannel;
} }
@@ -1,12 +1,10 @@
import { Command } from '@oclif/core'; import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core'; import { CombinedError } from '@urql/core';
import chalk from 'chalk';
import { GraphQLError } from 'graphql/error'; import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows'; import nullthrows from 'nullthrows';
import AnalyticsContextField from './context/AnalyticsContextField'; import AnalyticsContextField from './context/AnalyticsContextField';
import ContextField from './context/ContextField'; import ContextField from './context/ContextField';
import DynamicLoggedInContextField from './context/DynamicLoggedInContextField';
import { import {
DynamicPrivateProjectConfigContextField, DynamicPrivateProjectConfigContextField,
DynamicPublicProjectConfigContextField, DynamicPublicProjectConfigContextField,
@@ -16,8 +14,6 @@ import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField'; import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField';
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField'; import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField'; import ProjectDirContextField from './context/ProjectDirContextField';
import { ProjectIdContextField } from './context/ProjectIdContextField';
import { ServerSideEnvironmentVariablesContextField } from './context/ServerSideEnvironmentVariablesContextField';
import SessionManagementContextField from './context/SessionManagementContextField'; import SessionManagementContextField from './context/SessionManagementContextField';
import VcsClientContextField from './context/VcsClientContextField'; import VcsClientContextField from './context/VcsClientContextField';
import { EasCommandError } from './errors'; import { EasCommandError } from './errors';
@@ -26,10 +22,8 @@ import {
CommandEvent, CommandEvent,
createAnalyticsAsync, createAnalyticsAsync,
} from '../analytics/AnalyticsManager'; } from '../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../graphql/generated'; import Log from '../log';
import Log, { link } from '../log';
import SessionManager from '../user/SessionManager'; import SessionManager from '../user/SessionManager';
import { Client } from '../vcs/vcs';
export type ContextInput< export type ContextInput<
T extends { T extends {
@@ -47,26 +41,8 @@ export type ContextOutput<
[P in keyof T]: T[P]; [P in keyof T]: T[P];
}; };
type GetContextType<Type> = {
[Property in keyof Type]: any;
};
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.'; const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
interface BaseGetContextAsyncArgs {
nonInteractive: boolean;
vcsClientOverride?: Client;
}
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
extends BaseGetContextAsyncArgs {
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
}
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
withServerSideEnvironment?: never;
}
export default abstract class EasCommand extends Command { export default abstract class EasCommand extends Command {
protected static readonly ContextOptions = { protected static readonly ContextOptions = {
/** /**
@@ -83,16 +59,8 @@ export default abstract class EasCommand extends Command {
MaybeLoggedIn: { MaybeLoggedIn: {
maybeLoggedIn: new MaybeLoggedInContextField(), maybeLoggedIn: new MaybeLoggedInContextField(),
}, },
/**
* Specify this context if the logged-in requirement is only necessary in a particular execution of the command.
*/
DynamicLoggedIn: {
// eslint-disable-next-line async-protect/async-suffix
getDynamicLoggedInAsync: new DynamicLoggedInContextField(),
},
/** /**
* Specify this context requirement if the command needs to mutate the user session. * Specify this context requirement if the command needs to mutate the user session.
* @deprecated Should not be used outside of session management commands, which currently only includes `login` and `logout`.
*/ */
SessionManagment: { SessionManagment: {
sessionManager: new SessionManagementContextField(), sessionManager: new SessionManagementContextField(),
@@ -102,7 +70,7 @@ export default abstract class EasCommand extends Command {
* run within a project directory, null otherwise. * run within a project directory, null otherwise.
*/ */
OptionalProjectConfig: { OptionalProjectConfig: {
optionalPrivateProjectConfig: new OptionalPrivateProjectConfigContextField(), privateProjectConfig: new OptionalPrivateProjectConfigContextField(),
}, },
/** /**
* Require this command to be run in a project directory. Return the project directory in the context. * Require this command to be run in a project directory. Return the project directory in the context.
@@ -137,16 +105,6 @@ export default abstract class EasCommand extends Command {
Vcs: { Vcs: {
vcsClient: new VcsClientContextField(), vcsClient: new VcsClientContextField(),
}, },
ServerSideEnvironmentVariables: {
// eslint-disable-next-line async-protect/async-suffix
getServerSideEnvironmentVariablesAsync: new ServerSideEnvironmentVariablesContextField(),
},
/**
* Require the project to be identified and registered on server. Returns the project ID evaluated from the app config.
*/
ProjectId: {
projectId: new ProjectIdContextField(),
},
}; };
/** /**
@@ -181,19 +139,7 @@ export default abstract class EasCommand extends Command {
} = object, } = object,
>( >(
commandClass: { contextDefinition: ContextInput<C> }, commandClass: { contextDefinition: ContextInput<C> },
{ { nonInteractive }: { nonInteractive: boolean }
nonInteractive,
vcsClientOverride,
// if specified and not null, the env vars from the selected environment will be fetched from the server
// to resolve dynamic config (if dynamic config context is used) and enable getServerSideEnvironmentVariablesAsync function (if server side environment variables context is used)
withServerSideEnvironment,
}: C extends
| GetContextType<typeof EasCommand.ContextOptions.ProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.DynamicProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.OptionalProjectConfig>
| GetContextType<typeof EasCommand.ContextOptions.ServerSideEnvironmentVariables>
? GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
: GetContextAsyncArgsWithoutServerSideEnvironmentArgument
): Promise<ContextOutput<C>> { ): Promise<ContextOutput<C>> {
const contextDefinition = commandClass.contextDefinition; const contextDefinition = commandClass.contextDefinition;
@@ -206,8 +152,6 @@ export default abstract class EasCommand extends Command {
nonInteractive, nonInteractive,
sessionManager: this.sessionManager, sessionManager: this.sessionManager,
analytics: this.analytics, analytics: this.analytics,
vcsClientOverride,
withServerSideEnvironment,
}), }),
]); ]);
} }
@@ -240,13 +184,13 @@ export default abstract class EasCommand extends Command {
action: `eas ${this.id}`, action: `eas ${this.id}`,
}); });
return await this.runAsync(); return this.runAsync();
} }
// eslint-disable-next-line async-protect/async-suffix // eslint-disable-next-line async-protect/async-suffix
override async finally(err: Error): Promise<any> { override async finally(err: Error): Promise<any> {
await this.analytics.flushAsync(); await this.analytics.flushAsync();
return await super.finally(err); return super.finally(err);
} }
protected override catch(err: Error): Promise<any> { protected override catch(err: Error): Promise<any> {
@@ -254,7 +198,7 @@ export default abstract class EasCommand extends Command {
if (err instanceof EasCommandError) { if (err instanceof EasCommandError) {
Log.error(err.message); Log.error(err.message);
} else if (err instanceof CombinedError && err?.graphQLErrors) { } else if (err instanceof CombinedError && err?.graphQLErrors) {
const cleanGQLErrorsMessage = err?.graphQLErrors const cleanMessage = err?.graphQLErrors
.map((graphQLError: GraphQLError) => { .map((graphQLError: GraphQLError) => {
const messageLine = graphQLError.message.replace('[GraphQL] ', ''); const messageLine = graphQLError.message.replace('[GraphQL] ', '');
const requestIdLine = graphQLError.extensions?.requestId const requestIdLine = graphQLError.extensions?.requestId
@@ -264,31 +208,12 @@ export default abstract class EasCommand extends Command {
const defaultMsg = `${messageLine}${requestIdLine}`; const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') { if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
if (defaultMsg.includes('ScopedAccountActorViewerContext') && process.env.EAS_BUILD) { return `You don't have the required permissions to perform this operation.\n\n${defaultMsg}`;
// We're in EAS, authenticated with a scoped account actor access token.
// We may have not added the scoped actor privacy rule to the right place yet.
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nWe are in the process of migrating EAS to a more granular permissioning system. If you believe what you're doing is a legitimate operation you should be able to perform, report this to us at ${link(
'https://expo.dev/contact'
)}\n\nOriginal error message: ${defaultMsg}`;
}
return `${chalk.bold(
`You don't have the required permissions to perform this operation.`
)}\n\nThis can sometimes happen if you are logged in as incorrect user.\nRun ${chalk.bold(
'eas whoami'
)} to check the username you are logged in as.\nRun ${chalk.bold(
'eas login'
)} to change the account.\n\nOriginal error message: ${defaultMsg}`;
} }
return defaultMsg; return defaultMsg;
}) })
.join('\n'); .join('\n');
const cleanMessage = err.networkError
? `${cleanGQLErrorsMessage}\n${err.networkError.message}`
: cleanGQLErrorsMessage;
Log.error(cleanMessage); Log.error(cleanMessage);
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE; baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
} else { } else {
+1 -13
View File
@@ -38,13 +38,7 @@ export async function fetchBuildsAsync({
}: { }: {
graphqlClient: ExpoGraphqlClient; graphqlClient: ExpoGraphqlClient;
projectId: string; projectId: string;
filters?: { filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
statuses?: BuildStatus[];
platform?: RequestedPlatform;
profile?: string;
hasFingerprint?: boolean;
fingerprintHash?: string;
};
}): Promise<BuildFragment[]> { }): Promise<BuildFragment[]> {
let builds: BuildFragment[]; let builds: BuildFragment[];
const queryFilters: InputMaybe<BuildFilter> = {}; const queryFilters: InputMaybe<BuildFilter> = {};
@@ -54,12 +48,6 @@ export async function fetchBuildsAsync({
if (filters?.profile) { if (filters?.profile) {
queryFilters['buildProfile'] = filters.profile; queryFilters['buildProfile'] = filters.profile;
} }
if (filters?.hasFingerprint) {
queryFilters['hasFingerprint'] = filters.hasFingerprint;
}
if (filters?.fingerprintHash) {
queryFilters['fingerprintHash'] = filters.fingerprintHash;
}
if (!filters?.statuses) { if (!filters?.statuses) {
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, { builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
appId: projectId, appId: projectId,
@@ -1,17 +1,10 @@
import { Analytics } from '../../analytics/AnalyticsManager'; import { Analytics } from '../../analytics/AnalyticsManager';
import { EnvironmentVariableEnvironment } from '../../graphql/generated';
import SessionManager from '../../user/SessionManager'; import SessionManager from '../../user/SessionManager';
import { Client } from '../../vcs/vcs';
export interface ContextOptions { export interface ContextOptions {
sessionManager: SessionManager; sessionManager: SessionManager;
analytics: Analytics; analytics: Analytics;
nonInteractive: boolean; nonInteractive: boolean;
vcsClientOverride?: Client;
/**
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
*/
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
} }
export default abstract class ContextField<T> { export default abstract class ContextField<T> {
@@ -1,36 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating';
export type DynamicLoggedInContextFn = () => Promise<{
actor: Actor;
featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient;
authenticationInfo: LoggedInAuthenticationInfo;
}>;
export default class DynamicLoggedInContextField extends ContextField<DynamicLoggedInContextFn> {
async getValueAsync({
nonInteractive,
sessionManager,
}: ContextOptions): Promise<DynamicLoggedInContextFn> {
return async () => {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const featureGateServerValues: { [key: string]: boolean } = actor?.featureGates ?? {};
const graphqlClient = createGraphqlClient(authenticationInfo);
return {
actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient,
authenticationInfo,
};
};
}
}
@@ -1,14 +1,12 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config-types';
import ContextField, { ContextOptions } from './ContextField'; import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync'; import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync'; import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { import {
ExpoConfigOptions, ExpoConfigOptions,
getPrivateExpoConfigAsync, getPrivateExpoConfig,
getPublicExpoConfigAsync, getPublicExpoConfig,
} from '../../project/expoConfig'; } from '../../project/expoConfig';
export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{ export type DynamicConfigContextFn = (options?: ExpoConfigOptions) => Promise<{
@@ -21,34 +19,15 @@ export class DynamicPublicProjectConfigContextField extends ContextField<Dynamic
async getValueAsync({ async getValueAsync({
nonInteractive, nonInteractive,
sessionManager, sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> { }: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync(); const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => { return async (options?: ExpoConfigOptions) => {
const expBefore = await getPublicExpoConfigAsync(projectDir, options); const expBefore = getPublicExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, { const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive, nonInteractive,
env: options?.env, env: options?.env,
}); });
if (withServerSideEnvironment) { const exp = getPublicExpoConfig(projectDir, options);
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = await getPublicExpoConfigAsync(projectDir, options);
return { return {
exp, exp,
projectDir, projectDir,
@@ -62,34 +41,15 @@ export class DynamicPrivateProjectConfigContextField extends ContextField<Dynami
async getValueAsync({ async getValueAsync({
nonInteractive, nonInteractive,
sessionManager, sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<DynamicConfigContextFn> { }: ContextOptions): Promise<DynamicConfigContextFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync(); const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (options?: ExpoConfigOptions) => { return async (options?: ExpoConfigOptions) => {
const expBefore = await getPrivateExpoConfigAsync(projectDir, options); const expBefore = getPrivateExpoConfig(projectDir, options);
const projectId = await getProjectIdAsync(sessionManager, expBefore, { const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive, nonInteractive,
env: options?.env, env: options?.env,
}); });
if (withServerSideEnvironment) { const exp = getPrivateExpoConfig(projectDir, options);
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
options = {
...options,
env: {
...options?.env,
...serverSideEnvironmentVariables,
},
};
}
const exp = await getPrivateExpoConfigAsync(projectDir, options);
return { return {
exp, exp,
projectDir, projectDir,
@@ -1,22 +1,19 @@
import ContextField, { ContextOptions } from './ContextField'; import ContextField, { ContextOptions } from './ContextField';
import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient'; import { ExpoGraphqlClient, createGraphqlClient } from './contextUtils/createGraphqlClient';
import { LoggedInAuthenticationInfo } from '../../user/SessionManager';
import { Actor } from '../../user/User'; import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides'; import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating'; import FeatureGating from '../gating/FeatureGating';
type LoggedInContextType = { export default class LoggedInContextField extends ContextField<{
actor: Actor; actor: Actor;
featureGating: FeatureGating; featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient; graphqlClient: ExpoGraphqlClient;
authenticationInfo: LoggedInAuthenticationInfo; }> {
}; async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
actor: Actor;
export default class LoggedInContextField extends ContextField<LoggedInContextType> { featureGating: FeatureGating;
async getValueAsync({ graphqlClient: ExpoGraphqlClient;
nonInteractive, }> {
sessionManager,
}: ContextOptions): Promise<LoggedInContextType> {
const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({ const { actor, authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive, nonInteractive,
}); });
@@ -28,7 +25,6 @@ export default class LoggedInContextField extends ContextField<LoggedInContextTy
actor, actor,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()), featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient, graphqlClient,
authenticationInfo,
}; };
} }
} }
@@ -4,18 +4,16 @@ import { Actor } from '../../user/User';
import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides'; import FeatureGateEnvOverrides from '../gating/FeatureGateEnvOverrides';
import FeatureGating from '../gating/FeatureGating'; import FeatureGating from '../gating/FeatureGating';
type MaybeLoggedInContextType = { export default class MaybeLoggedInContextField extends ContextField<{
actor: Actor | null; actor: Actor | null;
featureGating: FeatureGating; featureGating: FeatureGating;
graphqlClient: ExpoGraphqlClient; graphqlClient: ExpoGraphqlClient;
authenticationInfo: { }> {
accessToken: string | null; async getValueAsync({ sessionManager }: ContextOptions): Promise<{
sessionSecret: string | null; actor: Actor | null;
}; featureGating: FeatureGating;
}; graphqlClient: ExpoGraphqlClient;
}> {
export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedInContextType> {
async getValueAsync({ sessionManager }: ContextOptions): Promise<MaybeLoggedInContextType> {
const authenticationInfo = { const authenticationInfo = {
accessToken: sessionManager.getAccessToken(), accessToken: sessionManager.getAccessToken(),
sessionSecret: sessionManager.getSessionSecret(), sessionSecret: sessionManager.getSessionSecret(),
@@ -29,7 +27,6 @@ export default class MaybeLoggedInContextField extends ContextField<MaybeLoggedI
actor: actor ?? null, actor: actor ?? null,
featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()), featureGating: new FeatureGating(featureGateServerValues, new FeatureGateEnvOverrides()),
graphqlClient, graphqlClient,
authenticationInfo,
}; };
} }
} }
@@ -1,12 +1,10 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config-types';
import { InvalidEasJsonError } from '@expo/eas-json/build/errors'; import { InvalidEasJsonError } from '@expo/eas-json/build/errors';
import ContextField, { ContextOptions } from './ContextField'; import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync'; import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync'; import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync'; import { getPrivateExpoConfig } from '../../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
export class OptionalPrivateProjectConfigContextField extends ContextField< export class OptionalPrivateProjectConfigContextField extends ContextField<
| { | {
@@ -16,11 +14,7 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
} }
| undefined | undefined
> { > {
async getValueAsync({ async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<
| { | {
projectId: string; projectId: string;
exp: ExpoConfig; exp: ExpoConfig;
@@ -41,24 +35,11 @@ export class OptionalPrivateProjectConfigContextField extends ContextField<
return undefined; return undefined;
} }
const expBefore = await getPrivateExpoConfigAsync(projectDir); const expBefore = getPrivateExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, { const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive, nonInteractive,
}); });
let serverSideEnvVars: Record<string, string> | undefined; const exp = getPrivateExpoConfig(projectDir);
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
return { return {
exp, exp,
projectDir, projectDir,
@@ -1,45 +1,26 @@
import { ExpoConfig } from '@expo/config'; import { ExpoConfig } from '@expo/config-types';
import ContextField, { ContextOptions } from './ContextField'; import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync'; import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync'; import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync'; import { getPrivateExpoConfig } from '../../project/expoConfig';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
export class PrivateProjectConfigContextField extends ContextField<{ export class PrivateProjectConfigContextField extends ContextField<{
projectId: string; projectId: string;
exp: ExpoConfig; exp: ExpoConfig;
projectDir: string; projectDir: string;
}> { }> {
async getValueAsync({ async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<{
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<{
projectId: string; projectId: string;
exp: ExpoConfig; exp: ExpoConfig;
projectDir: string; projectDir: string;
}> { }> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync(); const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = await getPrivateExpoConfigAsync(projectDir); const expBefore = getPrivateExpoConfig(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, { const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive, nonInteractive,
}); });
let serverSideEnvVars: Record<string, string> | undefined; const exp = getPrivateExpoConfig(projectDir);
if (withServerSideEnvironment) {
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
serverSideEnvVars = serverSideEnvironmentVariables;
}
const exp = await getPrivateExpoConfigAsync(projectDir, { env: serverSideEnvVars });
return { return {
projectId, projectId,
@@ -1,15 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { getPrivateExpoConfigAsync } from '../../project/expoConfig';
export class ProjectIdContextField extends ContextField<string> {
async getValueAsync({ nonInteractive, sessionManager }: ContextOptions): Promise<string> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const expBefore = await getPrivateExpoConfigAsync(projectDir);
const projectId = await getProjectIdAsync(sessionManager, expBefore, {
nonInteractive,
});
return projectId;
}
}
@@ -1,42 +0,0 @@
import ContextField, { ContextOptions } from './ContextField';
import { createGraphqlClient } from './contextUtils/createGraphqlClient';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from './contextUtils/getProjectIdAsync';
import { loadServerSideEnvironmentVariablesAsync } from './contextUtils/loadServerSideEnvironmentVariablesAsync';
import { getPublicExpoConfigAsync } from '../../project/expoConfig';
export type GetServerSideEnvironmentVariablesFn = (
maybeEnv?: Record<string, string>
) => Promise<Record<string, string>>;
export class ServerSideEnvironmentVariablesContextField extends ContextField<GetServerSideEnvironmentVariablesFn> {
async getValueAsync({
nonInteractive,
sessionManager,
withServerSideEnvironment,
}: ContextOptions): Promise<GetServerSideEnvironmentVariablesFn> {
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
return async (maybeEnv?: Record<string, string>) => {
if (!withServerSideEnvironment) {
throw new Error(
'withServerSideEnvironment parameter is required to evaluate ServerSideEnvironmentVariablesContextField'
);
}
const exp = await getPublicExpoConfigAsync(projectDir, { env: maybeEnv });
const projectId = await getProjectIdAsync(sessionManager, exp, {
nonInteractive,
env: maybeEnv,
});
const { authenticationInfo } = await sessionManager.ensureLoggedInAsync({
nonInteractive,
});
const graphqlClient = createGraphqlClient(authenticationInfo);
const serverSideEnvironmentVariables = await loadServerSideEnvironmentVariablesAsync({
environment: withServerSideEnvironment,
projectId,
graphqlClient,
});
return serverSideEnvironmentVariables;
};
}
}
@@ -1,18 +1,9 @@
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
import ContextField from './ContextField'; import ContextField from './ContextField';
import { findProjectDirAndVerifyProjectSetupAsync } from './contextUtils/findProjectDirAndVerifyProjectSetupAsync'; import { getVcsClient } from '../../vcs';
import { resolveVcsClient } from '../../vcs';
import { Client } from '../../vcs/vcs'; import { Client } from '../../vcs/vcs';
export default class VcsClientContextField extends ContextField<Client> { export default class VcsClientContextField extends ContextField<Client> {
async getValueAsync({ vcsClientOverride }: { vcsClientOverride?: Client }): Promise<Client> { async getValueAsync(): Promise<Client> {
if (vcsClientOverride) { return getVcsClient();
return vcsClientOverride;
}
const projectDir = await findProjectDirAndVerifyProjectSetupAsync();
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
return resolveVcsClient(config?.requireCommit);
} }
} }
@@ -6,7 +6,6 @@ import { Role } from '../../../../graphql/generated';
import { AppQuery } from '../../../../graphql/queries/AppQuery'; import { AppQuery } from '../../../../graphql/queries/AppQuery';
import { learnMore } from '../../../../log'; import { learnMore } from '../../../../log';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync'; import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { isExpoInstalled } from '../../../../project/projectUtils';
import SessionManager from '../../../../user/SessionManager'; import SessionManager from '../../../../user/SessionManager';
import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync'; import { findProjectRootAsync } from '../findProjectDirAndVerifyProjectSetupAsync';
import { getProjectIdAsync } from '../getProjectIdAsync'; import { getProjectIdAsync } from '../getProjectIdAsync';
@@ -22,7 +21,6 @@ jest.mock('../../../../ora', () => ({
}), }),
})); }));
jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync'); jest.mock('../../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
jest.mock('../../../../project/projectUtils');
describe(getProjectIdAsync, () => { describe(getProjectIdAsync, () => {
let sessionManager: SessionManager; let sessionManager: SessionManager;
@@ -57,7 +55,6 @@ describe(getProjectIdAsync, () => {
], ],
isExpoAdmin: false, isExpoAdmin: false,
featureGates: {}, featureGates: {},
preferences: {},
}, },
authenticationInfo: { accessToken: 'fake', sessionSecret: null }, authenticationInfo: { accessToken: 'fake', sessionSecret: null },
}); });
@@ -73,22 +70,15 @@ describe(getProjectIdAsync, () => {
); );
jest.mocked(findProjectRootAsync).mockResolvedValue('/app'); jest.mocked(findProjectRootAsync).mockResolvedValue('/app');
jest.mocked(isExpoInstalled).mockReturnValue(true);
}); });
it('gets the project ID from app config if exists', async () => { it('gets the project ID from app config if exists', async () => {
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@notnotbrent/test', fullName: '@notnotbrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any, ownerAccount: { name: 'notnotbrent' } as any,
}); });
@@ -96,7 +86,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManager, sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } }, { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false } { nonInteractive: false }
) )
).resolves.toEqual('1234'); ).resolves.toEqual('1234');
@@ -104,18 +94,11 @@ describe(getProjectIdAsync, () => {
it('throws when the owner is out of sync', async () => { it('throws when the owner is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
sdkVersion: '54.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@notnotbrent/test', fullName: '@notnotbrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any, ownerAccount: { name: 'notnotbrent' } as any,
}); });
@@ -123,13 +106,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManager, sessionManager,
{ { name: 'test', slug: 'test', owner: 'wat', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
owner: 'wat',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false } { nonInteractive: false }
) )
).rejects.toThrow( ).rejects.toThrow(
@@ -139,20 +116,13 @@ describe(getProjectIdAsync, () => {
); );
}); });
describe('when sdkVersion is less than 53', () => {
it('throws when the owner is not specified and is different than logged in user', async () => { it('throws when the owner is not specified and is different than logged in user', async () => {
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@totallybrent/test', fullName: '@totallybrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'totallybrent' } as any, ownerAccount: { name: 'totallybrent' } as any,
}); });
@@ -160,12 +130,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManager, sessionManager,
{ { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false } { nonInteractive: false }
) )
).rejects.toThrow( ).rejects.toThrow(
@@ -201,17 +166,11 @@ describe(getProjectIdAsync, () => {
const sessionManagerRobot = instance(sessionManagerMock); const sessionManagerRobot = instance(sessionManagerMock);
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@totallybrent/test', fullName: '@totallybrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'totallybrent' } as any, ownerAccount: { name: 'totallybrent' } as any,
}); });
@@ -219,12 +178,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManagerRobot, sessionManagerRobot,
{ { name: 'test', slug: 'test', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false } { nonInteractive: false }
) )
).rejects.toThrow( ).rejects.toThrow(
@@ -233,54 +187,14 @@ describe(getProjectIdAsync, () => {
)}` )}`
); );
}); });
});
describe('when sdkVersion is 53 or higher', () => {
it('does not throw when the owner is not specified', async () => {
jest.mocked(getConfig).mockReturnValue({
exp: {
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
} as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234',
fullName: '@totallybrent/test',
name: 'test',
slug: 'test',
ownerAccount: { name: 'totallybrent' } as any,
});
await expect(
getProjectIdAsync(
sessionManager,
{
sdkVersion: '53.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '1234' } },
},
{ nonInteractive: false }
)
).resolves.not.toThrow();
});
});
it('throws when the slug is out of sync', async () => { it('throws when the slug is out of sync', async () => {
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'wat',
extra: { eas: { projectId: '1234' } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@notnotbrent/test', fullName: '@notnotbrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any, ownerAccount: { name: 'notnotbrent' } as any,
}); });
@@ -288,7 +202,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManager, sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } }, { name: 'test', slug: 'wat', extra: { eas: { projectId: '1234' } } },
{ nonInteractive: false } { nonInteractive: false }
) )
).rejects.toThrow( ).rejects.toThrow(
@@ -299,17 +213,10 @@ describe(getProjectIdAsync, () => {
}); });
it('fetches the project ID when not in app config, and sets it in the config', async () => { it('fetches the project ID when not in app config, and sets it in the config', async () => {
jest jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({ jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'success', type: 'success',
config: { config: { expo: { name: 'test', slug: 'test', extra: { eas: { projectId: '2345' } } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: '2345' } },
},
}); });
jest jest
.mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync) .mocked(fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync)
@@ -317,7 +224,7 @@ describe(getProjectIdAsync, () => {
const projectId = await getProjectIdAsync( const projectId = await getProjectIdAsync(
sessionManager, sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' }, { name: 'test', slug: 'test' },
{ {
nonInteractive: false, nonInteractive: false,
} }
@@ -336,9 +243,7 @@ describe(getProjectIdAsync, () => {
}); });
it('throws if writing the ID back to the config fails', async () => { it('throws if writing the ID back to the config fails', async () => {
jest jest.mocked(getConfig).mockReturnValue({ exp: { name: 'test', slug: 'test' } } as any);
.mocked(getConfig)
.mockReturnValue({ exp: { sdkVersion: '52.0.0', name: 'test', slug: 'test' } } as any);
jest.mocked(modifyConfigAsync).mockResolvedValue({ jest.mocked(modifyConfigAsync).mockResolvedValue({
type: 'fail', type: 'fail',
config: null, config: null,
@@ -348,27 +253,17 @@ describe(getProjectIdAsync, () => {
.mockImplementation(async () => '4567'); .mockImplementation(async () => '4567');
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(sessionManager, { name: 'test', slug: 'test' }, { nonInteractive: false })
sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'test' },
{ nonInteractive: false }
)
).rejects.toThrow(); ).rejects.toThrow();
}); });
it('throws if extra.eas.projectId is not a string', async () => { it('throws if extra.eas.projectId is not a string', async () => {
jest.mocked(getConfig).mockReturnValue({ jest.mocked(getConfig).mockReturnValue({
exp: { exp: { name: 'test', slug: 'test', extra: { eas: { projectId: 1234 } } },
sdkVersion: '52.0.0',
name: 'test',
slug: 'test',
extra: { eas: { projectId: 1234 } },
},
} as any); } as any);
jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ jest.mocked(AppQuery.byIdAsync).mockResolvedValue({
id: '1234', id: '1234',
fullName: '@notnotbrent/test', fullName: '@notnotbrent/test',
name: 'test',
slug: 'test', slug: 'test',
ownerAccount: { name: 'notnotbrent' } as any, ownerAccount: { name: 'notnotbrent' } as any,
}); });
@@ -376,7 +271,7 @@ describe(getProjectIdAsync, () => {
await expect( await expect(
getProjectIdAsync( getProjectIdAsync(
sessionManager, sessionManager,
{ sdkVersion: '52.0.0', name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } }, { name: 'test', slug: 'wat', extra: { eas: { projectId: 1234 } } },
{ nonInteractive: false } { nonInteractive: false }
) )
).rejects.toThrow( ).rejects.toThrow(
@@ -38,7 +38,7 @@ export function createGraphqlClient(authInfo: {
return !!( return !!(
err && err &&
!operation.context.noRetry && !operation.context.noRetry &&
(err.networkError ?? err.graphQLErrors.some(e => e?.extensions?.isTransient)) (err.networkError || err.graphQLErrors.some(e => e?.extensions?.isTransient))
); );
}, },
}), }),
@@ -8,20 +8,20 @@ import semver from 'semver';
import { learnMore } from '../../../log'; import { learnMore } from '../../../log';
import { easCliVersion } from '../../../utils/easCli'; import { easCliVersion } from '../../../utils/easCli';
import { resolveVcsClient } from '../../../vcs'; import { getVcsClient, setVcsClient } from '../../../vcs';
import GitClient from '../../../vcs/clients/git';
async function applyCliConfigAsync(projectDir: string): Promise<void> { async function applyCliConfigAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir); const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor); const config = await EasJsonUtils.getCliConfigAsync(easJsonAccessor);
if (config?.version && !semver.satisfies(easCliVersion, config.version)) { if (config?.version && !semver.satisfies(easCliVersion, config.version)) {
throw new Error( throw new Error(
`You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint defined in eas.json (${ `You are on eas-cli@${easCliVersion} which does not satisfy the CLI version constraint in eas.json (${config.version})`
config.version
}).\n\nThis error probably means that you need update your eas-cli to a newer version.\nRun ${chalk.bold(
'npm install -g eas-cli'
)} to update the eas-cli to the latest version.`
); );
} }
if (config?.requireCommit) {
setVcsClient(new GitClient());
}
} }
async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> { async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise<void> {
@@ -42,7 +42,7 @@ async function ensureEasCliIsNotInDependenciesAsync(projectDir: string): Promise
consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`); consoleWarn(`Found ${chalk.bold('eas-cli')} in your project dependencies.`);
} }
const maybeRepoRoot = PackageManagerUtils.resolveWorkspaceRoot(projectDir) ?? projectDir; const maybeRepoRoot = PackageManagerUtils.findWorkspaceRoot(projectDir) ?? projectDir;
if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) { if (maybeRepoRoot !== projectDir && (await isEasCliInDependenciesAsync(maybeRepoRoot))) {
printCliVersionWarning = true; printCliVersionWarning = true;
consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`); consoleWarn(`Found ${chalk.bold('eas-cli')} in your monorepo dependencies.`);
@@ -92,7 +92,7 @@ export async function findProjectRootAsync({
} else { } else {
let vcsRoot; let vcsRoot;
try { try {
vcsRoot = path.normalize(await resolveVcsClient().getRootPathAsync()); vcsRoot = path.normalize(await getVcsClient().getRootPathAsync());
} catch {} } catch {}
if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) { if (vcsRoot && vcsRoot.startsWith(projectRootDir) && vcsRoot !== projectRootDir) {
throw new Error( throw new Error(
@@ -103,8 +103,6 @@ export async function findProjectRootAsync({
} }
} }
let ranEnsureEasCliIsNotInDependencies = false;
/** /**
* Determine the project root directory and ensure some constraints about the project setup * Determine the project root directory and ensure some constraints about the project setup
* like CLI version and dependencies. * like CLI version and dependencies.
@@ -115,9 +113,6 @@ let ranEnsureEasCliIsNotInDependencies = false;
export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> { export async function findProjectDirAndVerifyProjectSetupAsync(): Promise<string> {
const projectDir = await findProjectRootAsync(); const projectDir = await findProjectRootAsync();
await applyCliConfigAsync(projectDir); await applyCliConfigAsync(projectDir);
if (!ranEnsureEasCliIsNotInDependencies) {
ranEnsureEasCliIsNotInDependencies = true;
await ensureEasCliIsNotInDependenciesAsync(projectDir); await ensureEasCliIsNotInDependenciesAsync(projectDir);
}
return projectDir; return projectDir;
} }
@@ -1,18 +1,16 @@
import { ExpoConfig, getProjectConfigDescription } from '@expo/config'; import { getProjectConfigDescription } from '@expo/config';
import { ExpoConfig } from '@expo/config-types';
import { Env } from '@expo/eas-build-job'; import { Env } from '@expo/eas-build-job';
import chalk from 'chalk'; import chalk from 'chalk';
import semver from 'semver';
import { ExpoGraphqlClient, createGraphqlClient } from './createGraphqlClient'; import { createGraphqlClient } from './createGraphqlClient';
import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync'; import { findProjectRootAsync } from './findProjectDirAndVerifyProjectSetupAsync';
import { AppQuery } from '../../../graphql/queries/AppQuery'; import { AppQuery } from '../../../graphql/queries/AppQuery';
import Log, { learnMore } from '../../../log'; import Log, { learnMore } from '../../../log';
import { ora } from '../../../ora'; import { ora } from '../../../ora';
import { import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../../project/expoConfig';
createOrModifyExpoConfigAsync,
getPrivateExpoConfigAsync,
} from '../../../project/expoConfig';
import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync'; import { fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../../project/projectUtils';
import SessionManager from '../../../user/SessionManager'; import SessionManager from '../../../user/SessionManager';
import { Actor, getActorUsername } from '../../../user/User'; import { Actor, getActorUsername } from '../../../user/User';
@@ -20,15 +18,13 @@ import { Actor, getActorUsername } from '../../../user/User';
* Save an EAS project ID to the appropriate field in the app config. * Save an EAS project ID to the appropriate field in the app config.
* *
* @deprecated Should not be used outside of context functions except in the init command. * @deprecated Should not be used outside of context functions except in the init command.
* @deprecated Starting from `@expo/config` from SDK 52, the `modifyConfigAsync` function is merging existing data. Once this is released, we can use that instead of manually merging.
*/ */
export async function saveProjectIdToAppConfigAsync( export async function saveProjectIdToAppConfigAsync(
projectDir: string, projectDir: string,
projectId: string, projectId: string,
options: { env?: Env } = {} options: { env?: Env } = {}
): Promise<void> { ): Promise<void> {
// NOTE(cedric): we disable plugins to avoid writing plugin-generated content to `expo.extra` const exp = getPrivateExpoConfig(projectDir, options);
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true, ...options });
const result = await createOrModifyExpoConfigAsync( const result = await createOrModifyExpoConfigAsync(
projectDir, projectDir,
{ {
@@ -91,23 +87,6 @@ export async function getProjectIdAsync(
}); });
const graphqlClient = createGraphqlClient(authenticationInfo); const graphqlClient = createGraphqlClient(authenticationInfo);
const projectId = await validateOrSetProjectIdAsync({ exp, graphqlClient, actor, options });
return projectId;
}
export async function validateOrSetProjectIdAsync({
exp,
graphqlClient,
actor,
options,
cwd,
}: {
exp: ExpoConfig;
graphqlClient: ExpoGraphqlClient;
actor: Actor;
options: { env?: Env; nonInteractive: boolean };
cwd?: string;
}): Promise<string> {
const localProjectId = exp.extra?.eas?.projectId; const localProjectId = exp.extra?.eas?.projectId;
if (localProjectId) { if (localProjectId) {
if (typeof localProjectId !== 'string') { if (typeof localProjectId !== 'string') {
@@ -128,9 +107,6 @@ export async function validateOrSetProjectIdAsync({
); );
} }
const sdkVersion = exp.sdkVersion;
// SDK 53 and above no longer require owner field in app config
if (sdkVersion && semver.satisfies(sdkVersion, '< 53.0.0')) {
const actorUsername = getActorUsername(actor); const actorUsername = getActorUsername(actor);
if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) { if (!exp.owner && appForProjectId.ownerAccount.name !== actorUsername) {
if (actorUsername) { if (actorUsername) {
@@ -156,7 +132,6 @@ export async function validateOrSetProjectIdAsync({
); );
} }
} }
}
if (exp.slug && exp.slug !== appForProjectId.slug) { if (exp.slug && exp.slug !== appForProjectId.slug) {
throw new Error( throw new Error(
@@ -171,9 +146,7 @@ export async function validateOrSetProjectIdAsync({
return localProjectId; return localProjectId;
} }
const projectDir = await findProjectRootAsync({ const projectDir = await findProjectRootAsync();
cwd,
});
if (!projectDir) { if (!projectDir) {
throw new Error('This command must be run inside a project directory.'); throw new Error('This command must be run inside a project directory.');
} }
@@ -201,6 +174,7 @@ export async function validateOrSetProjectIdAsync({
{ {
accountName: getAccountNameForEASProjectSync(exp, actor), accountName: getAccountNameForEASProjectSync(exp, actor),
projectName: exp.slug, projectName: exp.slug,
privacy: toAppPrivacy(exp.privacy),
}, },
{ {
nonInteractive: options.nonInteractive, nonInteractive: options.nonInteractive,
@@ -1,59 +0,0 @@
import { ExpoGraphqlClient } from './createGraphqlClient';
import { EnvironmentVariableEnvironment } from '../../../graphql/generated';
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
import Log from '../../../log';
const cachedServerSideEnvironmentVariables: Record<
EnvironmentVariableEnvironment,
Record<string, string> | null
> = {
[EnvironmentVariableEnvironment.Development]: null,
[EnvironmentVariableEnvironment.Preview]: null,
[EnvironmentVariableEnvironment.Production]: null,
};
export async function loadServerSideEnvironmentVariablesAsync({
environment,
projectId,
graphqlClient,
}: {
environment: EnvironmentVariableEnvironment;
projectId: string;
graphqlClient: ExpoGraphqlClient;
}): Promise<Record<string, string>> {
// don't load environment variables if they were already loaded while executing a command
const cachedEnvVarsForEnvironment = cachedServerSideEnvironmentVariables[environment];
if (cachedEnvVarsForEnvironment) {
return cachedEnvVarsForEnvironment;
}
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.`
);
}
Log.newLine();
cachedServerSideEnvironmentVariables[environment] = serverEnvVars;
return serverEnvVars;
}
@@ -1,17 +1,5 @@
import { Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import { EnvironmentVariableEnvironment } from '../graphql/generated';
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
// are used in tandem, it saves on unnecessary typying in commands
async function upperCaseAsync<T>(input: string): Promise<T> {
return input.toUpperCase() as T;
}
function mapToLowercase<T extends string>(options: T[]): T[] {
return options.map(option => option.toLowerCase()) as T[];
}
export const EasNonInteractiveAndJsonFlags = { export const EasNonInteractiveAndJsonFlags = {
json: Flags.boolean({ json: Flags.boolean({
description: 'Enable JSON output, non-JSON messages will be printed to stderr.', description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
@@ -22,52 +10,6 @@ export const EasNonInteractiveAndJsonFlags = {
}), }),
}; };
export const EasEnvironmentFlagParameters = {
description: "Environment variable's environment",
parse: upperCaseAsync,
options: mapToLowercase([
EnvironmentVariableEnvironment.Development,
EnvironmentVariableEnvironment.Preview,
EnvironmentVariableEnvironment.Production,
]),
};
export const EASEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
};
export const EASMultiEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment>({
...EasEnvironmentFlagParameters,
multiple: true,
}),
};
export const EASVariableFormatFlag = {
format: Flags.enum({
description: 'Output format',
options: ['long', 'short'],
default: 'short',
}),
};
export const EASVariableVisibilityFlag = {
visibility: Flags.enum<'plaintext' | 'sensitive' | 'secret'>({
description: 'Visibility of the variable',
options: ['plaintext', 'sensitive', 'secret'],
}),
};
export type EASEnvironmentVariableScopeFlagValue = 'project' | 'account';
export const EASEnvironmentVariableScopeFlag = {
scope: Flags.enum<EASEnvironmentVariableScopeFlagValue>({
description: 'Scope for the variable',
options: ['project', 'account'],
default: 'project',
}),
};
export const EASNonInteractiveFlag = { export const EASNonInteractiveFlag = {
'non-interactive': Flags.boolean({ 'non-interactive': Flags.boolean({
description: 'Run the command in non-interactive mode.', description: 'Run the command in non-interactive mode.',
@@ -79,14 +21,3 @@ export const EasJsonOnlyFlag = {
description: 'Enable JSON output, non-JSON messages will be printed to stderr.', description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
}), }),
}; };
export const EasUpdateEnvironmentFlag = {
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
description:
'Environment to use for the server-side defined EAS environment variables during command execution.',
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
parse: upperCaseAsync,
required: false,
default: null,
}),
};
@@ -63,11 +63,7 @@ export default class FeatureGating {
} }
public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void { public static overrideKeyForEachInTest(key: FeatureGateKey, enabled: boolean): void {
beforeEach(() => { beforeEach(() => FeatureGateTestOverrides.setOverride(key, enabled));
FeatureGateTestOverrides.setOverride(key, enabled); afterEach(() => FeatureGateTestOverrides.removeOverride(key));
});
afterEach(() => {
FeatureGateTestOverrides.removeOverride(key);
});
} }
} }
+3 -27
View File
@@ -1,10 +1,7 @@
import { Errors, Flags } from '@oclif/core'; import { Flags } from '@oclif/core';
import chalk from 'chalk';
import EasCommand from '../../commandUtils/EasCommand'; import EasCommand from '../../commandUtils/EasCommand';
import Log from '../../log'; import Log from '../../log';
import { confirmAsync } from '../../prompts';
import { getActorDisplayName } from '../../user/User';
export default class AccountLogin extends EasCommand { export default class AccountLogin extends EasCommand {
static override description = 'log in with your Expo account'; static override description = 'log in with your Expo account';
@@ -16,11 +13,11 @@ export default class AccountLogin extends EasCommand {
description: 'Login with SSO', description: 'Login with SSO',
char: 's', char: 's',
default: false, default: false,
hidden: true,
}), }),
}; };
static override contextDefinition = { static override contextDefinition = {
...this.ContextOptions.MaybeLoggedIn,
...this.ContextOptions.SessionManagment, ...this.ContextOptions.SessionManagment,
}; };
@@ -29,28 +26,7 @@ export default class AccountLogin extends EasCommand {
flags: { sso }, flags: { sso },
} = await this.parse(AccountLogin); } = await this.parse(AccountLogin);
const { const { sessionManager } = await this.getContextAsync(AccountLogin, { nonInteractive: false });
sessionManager,
maybeLoggedIn: { actor },
} = await this.getContextAsync(AccountLogin, { nonInteractive: false });
if (sessionManager.getAccessToken()) {
throw new Error(
'EXPO_TOKEN is set in your environment, and is being used for all EAS authentication. To use username/password authentication, unset EXPO_TOKEN in your environment and re-run the command.'
);
}
if (actor) {
Log.warn(`You are already logged in as ${chalk.bold(getActorDisplayName(actor))}.`);
const shouldContinue = await confirmAsync({
message: 'Do you want to continue?',
});
if (!shouldContinue) {
Errors.error('Aborted', { exit: 1 });
}
}
await sessionManager.showLoginPromptAsync({ sso }); await sessionManager.showLoginPromptAsync({ sso });
Log.log('Logged in'); Log.log('Logged in');
} }
@@ -16,13 +16,10 @@ export default class AccountView extends EasCommand {
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { const {
maybeLoggedIn: { actor, authenticationInfo }, maybeLoggedIn: { actor },
} = await this.getContextAsync(AccountView, { nonInteractive: true }); } = await this.getContextAsync(AccountView, { nonInteractive: true });
if (actor) { if (actor) {
const loggedInAs = authenticationInfo.accessToken Log.log(chalk.green(getActorDisplayName(actor)));
? `${getActorDisplayName(actor)} (authenticated using EXPO_TOKEN)`
: getActorDisplayName(actor);
Log.log(chalk.green(loggedInAs));
// personal account is included, only show if more accounts that personal account // personal account is included, only show if more accounts that personal account
// but do show personal account in list if there are more // but do show personal account in list if there are more
+1 -1
View File
@@ -10,7 +10,7 @@ export default class AnalyticsView extends EasCommand {
async runAsync(): Promise<void> { async runAsync(): Promise<void> {
const { STATUS: status } = (await this.parse(AnalyticsView)).args; const { STATUS: status } = (await this.parse(AnalyticsView)).args;
if (status) { if (status) {
await setAnalyticsEnabledAsync(status === 'on'); setAnalyticsEnabledAsync(status === 'on');
Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`); Log.withTick(`${status === 'on' ? 'Enabling' : 'Disabling'} analytics.`);
} else { } else {
const analyticsEnabled = await getAnalyticsEnabledAsync(); const analyticsEnabled = await getAnalyticsEnabledAsync();
@@ -25,7 +25,7 @@ export default class BranchCreate extends EasCommand {
}; };
static override contextDefinition = { static override contextDefinition = {
...this.ContextOptions.ProjectId, ...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn, ...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs, ...this.ContextOptions.Vcs,
}; };
@@ -36,7 +36,7 @@ export default class BranchCreate extends EasCommand {
flags: { json: jsonFlag, 'non-interactive': nonInteractive }, flags: { json: jsonFlag, 'non-interactive': nonInteractive },
} = await this.parse(BranchCreate); } = await this.parse(BranchCreate);
const { const {
projectId, privateProjectConfig: { projectId },
loggedIn: { graphqlClient }, loggedIn: { graphqlClient },
vcsClient, vcsClient,
} = await this.getContextAsync(BranchCreate, { } = await this.getContextAsync(BranchCreate, {
@@ -58,7 +58,7 @@ export default class BranchCreate extends EasCommand {
type: 'text', type: 'text',
name: 'name', name: 'name',
message: 'Provide a branch name:', message: 'Provide a branch name:',
initial: (await getDefaultBranchNameAsync(vcsClient)) ?? undefined, initial: await getDefaultBranchNameAsync(vcsClient),
validate: value => (value ? true : validationMessage), validate: value => (value ? true : validationMessage),
})); }));
} }
Loaded 100 of 491 files, more files were not shown because too many files have changed in this diff. Show more