mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-13 20:28:08 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a669bccb0 | ||
|
|
d33cfaba67 | ||
|
|
7363164889 |
No files matched your search
@@ -5,4 +5,3 @@
|
||||
packages/eas-cli/src/graphql/generated.ts
|
||||
|
||||
node_modules
|
||||
ts-declarations
|
||||
@@ -30,6 +30,12 @@ module.exports = {
|
||||
},
|
||||
],
|
||||
'no-underscore-dangle': ['error', { allow: ['__typename'] }],
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
'async-protect/async-suffix': 'error',
|
||||
'node/no-sync': 'error',
|
||||
},
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
commentConfiguration:
|
||||
preamble: 'Subscribed to pull request'
|
||||
epilogue: 'Generated by [CodeMention](https://github.com/expo/expo/actions/workflows/codemention.yaml)'
|
||||
rules:
|
||||
- patterns: ['**/*']
|
||||
mentions: ['douglowder']
|
||||
- patterns: ['packages/eas-cli/src/update/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/commands/update/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/channel/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/src/commands/channel/**']
|
||||
mentions: ['wschurman', 'quinlanj']
|
||||
- patterns: ['packages/eas-cli/schema/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-cli/src/metadata/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-cli/src/commands/metadata/**']
|
||||
mentions: ['bycedric']
|
||||
- patterns: ['packages/eas-cli/src/worker/**']
|
||||
mentions: ['kitten', 'kadikraman']
|
||||
- patterns: ['packages/eas-cli/src/commands/deploy/**']
|
||||
mentions: ['kitten', 'kadikraman']
|
||||
- patterns: ['packages/eas-cli/src/commands/build/**']
|
||||
mentions: ['sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/build/**']
|
||||
mentions: ['sjchmiela']
|
||||
- patterns: ['packages/eas-cli/src/commands/workflow/**']
|
||||
mentions: ['sjchmiela']
|
||||
@@ -1,17 +0,0 @@
|
||||
name: build-android
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Android
|
||||
type: build
|
||||
runs_on: linux-large
|
||||
params:
|
||||
platform: android
|
||||
profile: system-test
|
||||
custom_job:
|
||||
name: Custom Job
|
||||
steps:
|
||||
- name: Echo
|
||||
run: echo "This is a custom job step"
|
||||
@@ -1,16 +0,0 @@
|
||||
name: setup-gcloud
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Auth gcloud
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: 'projects/321830142373/locations/global/workloadIdentityPools/github/providers/expo'
|
||||
project_id: exponentjs
|
||||
|
||||
- name: Setup gcloud
|
||||
uses: google-github-actions/setup-gcloud@e427ad8a34f8676edf47cf7d7925499adf3eb74f # v2
|
||||
with:
|
||||
version: 548.0.0
|
||||
project_id: exponentjs
|
||||
@@ -1,205 +0,0 @@
|
||||
name: build-and-deploy-worker
|
||||
# Assumes main branch always specifies an environment (staging/production).
|
||||
# If environment is empty: PR testing build (no upload, no deploy)
|
||||
# If environment is set: Build (unless cached on main), upload, and deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
environment:
|
||||
description: '"staging", "production" or empty'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
environment:
|
||||
description: '"staging", "production" or empty'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: volta-cli/action@5c175f92dea6f48441c436471e6479dbc192e194 # v4
|
||||
|
||||
- uses: ./.github/internal-actions/setup-gcloud
|
||||
|
||||
- name: Check if worker already exists
|
||||
id: check_worker_exists
|
||||
# Check if tarballs for this SHA already exist
|
||||
# If they do, we can skip building regardless of branch
|
||||
if: inputs.environment != ''
|
||||
run: |
|
||||
for platform in android ios; do
|
||||
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
|
||||
|
||||
if ! gsutil stat gs://eas-build-worker-tarballs/$GCP_TARBALL; then
|
||||
echo "worker_exists=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
echo "worker_exists=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Start EAS Workflow to build worker tarballs
|
||||
id: eas_workflow
|
||||
# Build if: PR testing (no env), or deploying but tarballs don't exist
|
||||
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Only upload artifacts from EAS if GHA will upload them to GCS
|
||||
# This avoids unnecessary artifact uploads for PR builds
|
||||
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
|
||||
|
||||
# Run workflow and capture output (includes run ID)
|
||||
WORKFLOW_OUTPUT=$(npx -y eas-cli@latest workflow:run build-worker.yml \
|
||||
--ref ${{ github.sha }} \
|
||||
--input upload_artifact=$SHOULD_UPLOAD_ARTIFACT \
|
||||
--non-interactive \
|
||||
--no-wait \
|
||||
--json)
|
||||
|
||||
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_OUTPUT" | jq -r '.id')
|
||||
if [ -z "$WORKFLOW_RUN_ID" ] || [ "$WORKFLOW_RUN_ID" = "null" ]; then
|
||||
echo "Error: Could not determine workflow run ID"
|
||||
echo "Workflow output:"
|
||||
echo "$WORKFLOW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "workflow_run_id=$WORKFLOW_RUN_ID" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Wait for EAS workflow and capture artifacts
|
||||
id: eas_artifacts
|
||||
if: inputs.environment == '' || steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
|
||||
if [ -z "$WORKFLOW_RUN_ID" ]; then
|
||||
echo "Error: Missing workflow run ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SHOULD_UPLOAD_ARTIFACT="${{ inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false' }}"
|
||||
|
||||
while true; do
|
||||
VIEW_OUTPUT=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" \
|
||||
--non-interactive \
|
||||
--json)
|
||||
STATUS=$(echo "$VIEW_OUTPUT" | jq -r '.status')
|
||||
|
||||
case "$STATUS" in
|
||||
SUCCESS)
|
||||
break
|
||||
;;
|
||||
FAILURE)
|
||||
echo "EAS workflow failed."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
CANCELED)
|
||||
echo "EAS workflow canceled."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
ACTION_REQUIRED)
|
||||
echo "EAS workflow requires action."
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
NEW|IN_PROGRESS)
|
||||
sleep 7
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected workflow status: $STATUS"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Skip artifact extraction if we didn't request uploads
|
||||
if [ "$SHOULD_UPLOAD_ARTIFACT" == "false" ]; then
|
||||
echo "Skipping artifact download - upload was disabled for this run"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Extract artifact download URLs from workflow output
|
||||
# Artifacts are in jobs[].artifacts[] for custom jobs
|
||||
DOWNLOAD_IOS_URL=$(echo "$VIEW_OUTPUT" | jq -r '
|
||||
.jobs[]
|
||||
| .artifacts[]?
|
||||
| select(.name == "worker-ios-tarball")
|
||||
| .downloadUrl
|
||||
' | head -1)
|
||||
|
||||
DOWNLOAD_ANDROID_URL=$(echo "$VIEW_OUTPUT" | jq -r '
|
||||
.jobs[]
|
||||
| .artifacts[]?
|
||||
| select(.name == "worker-android-tarball")
|
||||
| .downloadUrl
|
||||
' | head -1)
|
||||
|
||||
if [ -z "$DOWNLOAD_IOS_URL" ] || [ "$DOWNLOAD_IOS_URL" = "null" ]; then
|
||||
echo "Error: Could not find iOS artifact download URL"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$DOWNLOAD_ANDROID_URL" ] || [ "$DOWNLOAD_ANDROID_URL" = "null" ]; then
|
||||
echo "Error: Could not find Android artifact download URL"
|
||||
echo "$VIEW_OUTPUT" | jq .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Mask the download URLs
|
||||
echo "::add-mask::$DOWNLOAD_IOS_URL"
|
||||
echo "::add-mask::$DOWNLOAD_ANDROID_URL"
|
||||
echo "download_ios_url=$DOWNLOAD_IOS_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "download_android_url=$DOWNLOAD_ANDROID_URL" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Cancel EAS workflow on GitHub cancellation
|
||||
if: cancelled()
|
||||
working-directory: packages/worker
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.EXPO_DEV_EXPO_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
WORKFLOW_RUN_ID="${{ steps.eas_workflow.outputs.workflow_run_id }}"
|
||||
if [ -n "$WORKFLOW_RUN_ID" ]; then
|
||||
npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive
|
||||
fi
|
||||
|
||||
- name: Upload worker tarballs to GCP
|
||||
# Upload when deploying if we built something new
|
||||
# Don't upload PR builds (no environment) or when using cached tarballs
|
||||
if: inputs.environment != '' && steps.check_worker_exists.outputs.worker_exists == 'false'
|
||||
run: |
|
||||
IOS_GCP_TARBALL="worker-ios-sha1-$GITHUB_SHA.tar.gz"
|
||||
ANDROID_GCP_TARBALL="worker-android-sha1-$GITHUB_SHA.tar.gz"
|
||||
curl -L "${{ steps.eas_artifacts.outputs.download_ios_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$IOS_GCP_TARBALL
|
||||
curl -L "${{ steps.eas_artifacts.outputs.download_android_url }}" | gsutil cp - gs://eas-build-worker-tarballs/$ANDROID_GCP_TARBALL
|
||||
|
||||
- name: Promote worker to ${{ inputs.environment }}
|
||||
if: inputs.environment != ''
|
||||
run: |
|
||||
for platform in android ios; do
|
||||
GCP_TARBALL="worker-$platform-sha1-$GITHUB_SHA.tar.gz"
|
||||
WORKER_STAGING_TARBALL="worker-$platform-${{ inputs.environment }}.tar.gz"
|
||||
|
||||
gsutil cp gs://eas-build-worker-tarballs/$GCP_TARBALL gs://eas-build-worker-tarballs/$WORKER_STAGING_TARBALL
|
||||
done
|
||||
@@ -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
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check if CHANGELOG.md was updated
|
||||
id: changelog-updated
|
||||
uses: tj-actions/changed-files@b1ba699b304f2083b602164e06a89b868c84f076
|
||||
uses: tj-actions/changed-files@v41
|
||||
with:
|
||||
files: CHANGELOG.md
|
||||
- name: Fail if CHANGELOG.md was not updated and the "no changelog" label is absent
|
||||
@@ -36,5 +36,6 @@ jobs:
|
||||
refresh-message-position: true
|
||||
message: ${{ contains(github.event.pull_request.labels.*.name, 'no changelog') && '⏩ The changelog entry check has been skipped since the "no changelog" label is present.' || '✅ Thank you for adding the changelog entry!' }}
|
||||
message-failure: |
|
||||
❌ It looks like a changelog entry is missing for this PR. Add it manually to CHANGELOG.md.
|
||||
❌ It looks like a changelog entry is missing for this PR. You have two options: you can add it manually, or you can use the changelog bot to do it for you.
|
||||
🤖 To use the bot, simply comment on this PR with the command `/changelog-entry [breaking-change|new-feature|bug-fix|chore] [message]`.
|
||||
⏩ If this PR doesn't require a changelog entry, such as if it's an internal change that doesn't affect the user experience, you can add the "no changelog" label to the PR.
|
||||
@@ -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 }}
|
||||
@@ -1,64 +0,0 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
staging_only:
|
||||
type: boolean
|
||||
required: true
|
||||
default: false
|
||||
description: "Only move the latest-eas-build-staging tag, not the latest-eas-build tag"
|
||||
version:
|
||||
type: string
|
||||
required: true
|
||||
description: "Version number to move the tag to"
|
||||
dry_run:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: "Run the workflow in dry run mode, without making any NPM changes"
|
||||
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
|
||||
jobs:
|
||||
move-tag:
|
||||
name: Move NPM tags used by EAS builds and workflows
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
INPUT_STAGING_ONLY: ${{ github.event.inputs.staging_only }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: "https://registry.npmjs.org/"
|
||||
scope: "expo"
|
||||
node-version: 22
|
||||
- name: Resolve version (fails if the version does not exist)
|
||||
run: |
|
||||
set -o pipefail
|
||||
npm pack --dry-run eas-cli@$INPUT_VERSION
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build-staging"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
cmd="npm dist-tag add eas-cli@$INPUT_VERSION latest-eas-build"
|
||||
if [[ "$INPUT_DRY_RUN" == "true" ]]; then
|
||||
echo "Running in dry run mode, skipping version check"
|
||||
echo "cmd = $cmd"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$INPUT_STAGING_ONLY" == "true" ]]; then
|
||||
echo "Only moving the staging tag, skipping the production tag"
|
||||
exit 0
|
||||
fi
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
$cmd
|
||||
@@ -9,8 +9,6 @@ jobs:
|
||||
release:
|
||||
name: Create a release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -20,25 +18,18 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Ensure GraphQL schema and generated code is up-to-date
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
yarn verify-graphql-code
|
||||
- name: Build tarballs
|
||||
working-directory: ${{ github.workspace }}/packages/eas-cli
|
||||
run: |
|
||||
@@ -73,16 +64,11 @@ jobs:
|
||||
# Use macOS to get the correct platform specific dependencies
|
||||
runs-on: macos-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- run: brew install python-setuptools
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn build
|
||||
- name: Build tarballs
|
||||
@@ -118,13 +104,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: sudo apt-get install nsis
|
||||
- run: yarn build
|
||||
@@ -160,42 +144,46 @@ jobs:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Publish packages to npm
|
||||
run: yarn lerna publish from-package --yes --no-verify-access
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build-staging tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build-staging
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
- name: Add latest-eas-build tag
|
||||
run: |
|
||||
VERSION=$(cat lerna.json | jq -r .version)
|
||||
echo //registry.npmjs.org/:_authToken=$NODE_AUTH_TOKEN > ~/.npmrc
|
||||
npm dist-tag add eas-cli@$VERSION latest-eas-build
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
release-changelog:
|
||||
name: Update changelog and publish release
|
||||
runs-on: ubuntu-latest
|
||||
needs: publish-to-npm
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
- name: Install dependencies
|
||||
working-directory: ./scripts
|
||||
run: yarn install --frozen-lockfile --check-files
|
||||
@@ -215,7 +203,7 @@ jobs:
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
git add CHANGELOG.md
|
||||
git commit --allow-empty -m "update CHANGELOG.md"
|
||||
git commit -m "update CHANGELOG.md"
|
||||
git push
|
||||
- name: Prepare changelog for Slack
|
||||
run: |
|
||||
@@ -235,3 +223,22 @@ jobs:
|
||||
SLACK_USERNAME: 'EAS CLI CI'
|
||||
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
|
||||
SLACK_FOOTER: ''
|
||||
open-pr-in-universe:
|
||||
name: Open a PR with `@expo/eas-json` bump in universe
|
||||
needs: publish-to-npm
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
eas-json:
|
||||
- 'packages/eas-json/**'
|
||||
- name: Open a PR with `@expo/eas-json` version bump in universe
|
||||
if: steps.filter.outputs.eas-json == 'true'
|
||||
uses: benc-uk/workflow-dispatch@v1
|
||||
with:
|
||||
repo: expo/universe
|
||||
ref: main
|
||||
workflow: update-eas-json.yml
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Ensure GraphQL schema and generated code is up-to-date
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
schedule:
|
||||
- cron: '0 8 * * *' # 8:00 AM UTC time every day
|
||||
|
||||
concurrency:
|
||||
group: test-graphql-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Ensure GraphQL schema and generated code is up-to-date
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 22
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn verify-graphql-code
|
||||
working-directory: packages/eas-cli
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['20', '22', '24']
|
||||
node: ['16', '18', '20']
|
||||
include:
|
||||
- node: '20'
|
||||
coverage: true
|
||||
@@ -28,9 +28,6 @@ jobs:
|
||||
node-version: ${{ matrix.node }}
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
- run: yarn typecheck
|
||||
- run: |
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Test"
|
||||
- run: yarn test
|
||||
if: ${{ !matrix.coverage }}
|
||||
env:
|
||||
@@ -52,7 +49,7 @@ jobs:
|
||||
|
||||
notify-slack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test]
|
||||
needs: test
|
||||
name: Notify Slack
|
||||
if: ${{ github.ref == 'refs/heads/main' && always() }}
|
||||
steps:
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
type: string
|
||||
required: false
|
||||
description: 'Version number for the release'
|
||||
dry_run:
|
||||
type: boolean
|
||||
required: true
|
||||
default: true
|
||||
description: 'Run the release in dry run mode, without pushing any changes'
|
||||
|
||||
name: Manually trigger a release
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Trigger a release
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.EXPO_BOT_PAT }}
|
||||
INPUT_VERSION: ${{ github.event.inputs.version }}
|
||||
INPUT_DRY_RUN: ${{ github.event.inputs.dry_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: |
|
||||
# Lerna will not detect changes correctly without having all tags
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
# Checkout the main branch
|
||||
git checkout main
|
||||
git config --global user.email "[email protected]"
|
||||
git config --global user.name "Expo CI"
|
||||
yarn install --frozen-lockfile --check-files
|
||||
yarn build
|
||||
if [[ "$INPUT_VERSION" == "" ]]; then
|
||||
echo "Releasing with default version"
|
||||
yarn release
|
||||
else
|
||||
echo "Releasing with version $INPUT_VERSION"
|
||||
yarn release $INPUT_VERSION
|
||||
fi
|
||||
@@ -66,7 +66,7 @@ jobs:
|
||||
destination_branch: 'main'
|
||||
pr_title: 'Upgrade eas-build dependencies'
|
||||
pr_body: ':arrow_up: Upgrades [`eas-build`](https://github.com/expo/eas-build) dependencies.'
|
||||
pr_reviewer: 'sjchmiela'
|
||||
pr_reviewer: 'szdziedzic,radoslawkrzemien,sjchmiela,khamilowicz'
|
||||
github_token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
|
||||
- uses: actions/github-script@v3
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
name: system-tests
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
workflow_call: {}
|
||||
|
||||
jobs:
|
||||
system-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
EXPO_TOKEN: ${{ secrets.STAGING_EXPO_DEV_EXPO_SERVICES_GITHUB_ROBOT_ACCESS_TOKEN }}
|
||||
EXPO_STAGING: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: volta-cli/action@5c175f92dea6f48441c436471e6479dbc192e194 # v4
|
||||
|
||||
- name: Create test app
|
||||
run: npx -y create-expo-app@latest ${{ runner.temp }}/test-app --template tabs
|
||||
|
||||
- name: Link to EAS project
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: npx -y eas-cli@latest init --id 6ae8028d-4e6d-4912-8eb2-87d4e6122880 --force --non-interactive
|
||||
|
||||
- name: Add bundle identifiers to app.json
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
jq '.expo.ios.bundleIdentifier = "dev.expo.workersystemtests" | .expo.android.package = "dev.expo.workersystemtests"' app.json > app.json.tmp
|
||||
mv app.json.tmp app.json
|
||||
|
||||
- name: Configure EAS Build
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: npx -y eas-cli@latest build:configure --platform all
|
||||
|
||||
- name: Add system-test profile to eas.json
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
jq '.build["system-test"] = { "ios": { "simulator": true }, "android": { "withoutCredentials": true } }' eas.json > eas.json.tmp
|
||||
mv eas.json.tmp eas.json
|
||||
|
||||
- name: Setup EAS workflow
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
mkdir -p .eas/workflows
|
||||
cp ${{ github.workspace }}/.github/fixtures/eas-workflow-build-android.yml .eas/workflows/build-android.yml
|
||||
|
||||
- name: Run iOS build and Android workflow concurrently
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_JSON=$(npx -y eas-cli@latest build --platform ios --profile system-test --non-interactive --no-wait --json)
|
||||
BUILD_ID=$(echo "$BUILD_JSON" | jq -r 'if type=="array" then .[0].id // .[0].build.id // .[0].buildId else .id // .build.id // .buildId end // empty')
|
||||
if [ -z "$BUILD_ID" ]; then
|
||||
echo "Error: could not determine build ID"
|
||||
echo "$BUILD_JSON" | jq .
|
||||
exit 1
|
||||
fi
|
||||
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_ENV"
|
||||
echo "Resolved BUILD_ID=$BUILD_ID"
|
||||
|
||||
WORKFLOW_JSON=$(npx -y eas-cli@latest workflow:run build-android.yml --non-interactive --no-wait --json)
|
||||
WORKFLOW_RUN_ID=$(echo "$WORKFLOW_JSON" | jq -r '.id // .workflowRun.id // .workflowRunId // empty')
|
||||
if [ -z "$WORKFLOW_RUN_ID" ]; then
|
||||
echo "Error: could not determine workflow run ID"
|
||||
echo "$WORKFLOW_JSON" | jq .
|
||||
exit 1
|
||||
fi
|
||||
echo "WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID" >> "$GITHUB_ENV"
|
||||
echo "Resolved WORKFLOW_RUN_ID=$WORKFLOW_RUN_ID"
|
||||
|
||||
is_terminal_build_status() {
|
||||
case "$1" in
|
||||
FINISHED|ERRORED|CANCELED) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_terminal_workflow_status() {
|
||||
case "$1" in
|
||||
SUCCESS|FAILURE|CANCELED|ACTION_REQUIRED) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
BUILD_STATUS=""
|
||||
WORKFLOW_STATUS=""
|
||||
TIMEOUT_SECONDS=1800
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
while true; do
|
||||
ELAPSED=$(( $(date +%s) - START_TIME ))
|
||||
if [ "$ELAPSED" -ge "$TIMEOUT_SECONDS" ]; then
|
||||
echo "Timeout: polling exceeded 30 minutes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json | jq -r '.status // empty')
|
||||
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json | jq -r '.status // empty')
|
||||
|
||||
echo "iOS build status: $BUILD_STATUS"
|
||||
echo "Android workflow status: $WORKFLOW_STATUS"
|
||||
|
||||
if is_terminal_build_status "$BUILD_STATUS" && is_terminal_workflow_status "$WORKFLOW_STATUS"; then
|
||||
break
|
||||
fi
|
||||
|
||||
sleep 7
|
||||
done
|
||||
|
||||
EXIT_CODE=0
|
||||
|
||||
if [ "$BUILD_STATUS" != "FINISHED" ]; then
|
||||
echo "Build did not finish successfully: $BUILD_STATUS"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
if [ "$WORKFLOW_STATUS" != "SUCCESS" ]; then
|
||||
echo "Workflow did not finish successfully: $WORKFLOW_STATUS"
|
||||
EXIT_CODE=1
|
||||
fi
|
||||
|
||||
exit "$EXIT_CODE"
|
||||
|
||||
- name: Cancel EAS runs on workflow cancellation
|
||||
if: always() && (cancelled() || failure())
|
||||
working-directory: ${{ runner.temp }}/test-app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -n "${BUILD_ID:-}" ]; then
|
||||
echo "Canceling EAS build: $BUILD_ID"
|
||||
BUILD_CANCEL_OUTPUT=$(npx -y eas-cli@latest build:cancel "$BUILD_ID" --non-interactive 2>&1 || true)
|
||||
if [ -n "$BUILD_CANCEL_OUTPUT" ]; then
|
||||
echo "$BUILD_CANCEL_OUTPUT"
|
||||
else
|
||||
echo "build:cancel produced no output"
|
||||
fi
|
||||
BUILD_STATUS=$(npx -y eas-cli@latest build:view "$BUILD_ID" --json 2>/dev/null | jq -r '.status // empty' || true)
|
||||
if [ -n "$BUILD_STATUS" ]; then
|
||||
echo "Post-cancel build status: $BUILD_STATUS"
|
||||
else
|
||||
echo "Post-cancel build status unavailable"
|
||||
fi
|
||||
fi
|
||||
if [ -n "${WORKFLOW_RUN_ID:-}" ]; then
|
||||
echo "Canceling EAS workflow run: $WORKFLOW_RUN_ID"
|
||||
WORKFLOW_CANCEL_OUTPUT=$(npx -y eas-cli@latest workflow:cancel "$WORKFLOW_RUN_ID" --non-interactive 2>&1 || true)
|
||||
if [ -n "$WORKFLOW_CANCEL_OUTPUT" ]; then
|
||||
echo "$WORKFLOW_CANCEL_OUTPUT"
|
||||
else
|
||||
echo "workflow:cancel produced no output"
|
||||
fi
|
||||
WORKFLOW_STATUS=$(npx -y eas-cli@latest workflow:view "$WORKFLOW_RUN_ID" --non-interactive --json 2>/dev/null | jq -r '.status // empty' || true)
|
||||
if [ -n "$WORKFLOW_STATUS" ]; then
|
||||
echo "Post-cancel workflow status: $WORKFLOW_STATUS"
|
||||
else
|
||||
echo "Post-cancel workflow status unavailable"
|
||||
fi
|
||||
fi
|
||||
@@ -1,75 +0,0 @@
|
||||
name: worker
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "packages/worker/**"
|
||||
- .github/workflows/worker.yml
|
||||
- .github/workflows/build-and-deploy-worker.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- "packages/worker/**"
|
||||
- .github/workflows/worker.yml
|
||||
- .github/workflows/build-and-deploy-worker.yml
|
||||
|
||||
concurrency:
|
||||
group: worker-${{ github.ref }}-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
worker-checks:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- uses: volta-cli/action@5c175f92dea6f48441c436471e6479dbc192e194 # v4
|
||||
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
|
||||
- name: Run unit tests
|
||||
working-directory: packages/worker
|
||||
run: yarn test:unit
|
||||
|
||||
- name: Run integration tests
|
||||
working-directory: packages/worker
|
||||
run: yarn test:integration
|
||||
|
||||
worker-build:
|
||||
needs:
|
||||
- worker-checks
|
||||
uses: ./.github/workflows/build-and-deploy-worker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
environment: ${{ github.ref == 'refs/heads/main' && 'staging' || '' }}
|
||||
|
||||
system-tests-staging:
|
||||
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
|
||||
needs:
|
||||
- worker-build
|
||||
- worker-checks
|
||||
uses: ./.github/workflows/worker-system-tests.yml
|
||||
secrets: inherit
|
||||
|
||||
deploy-worker-production:
|
||||
if: github.ref == 'refs/heads/main' && !failure() && !cancelled()
|
||||
needs:
|
||||
- system-tests-staging
|
||||
uses: ./.github/workflows/build-and-deploy-worker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
environment: production
|
||||
|
||||
notify-slack:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- deploy-worker-production
|
||||
if: github.ref == 'refs/heads/main' && always() && !cancelled()
|
||||
steps:
|
||||
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
|
||||
|
||||
- name: Notify Slack
|
||||
uses: ./.github/internal-actions/notify-slack-on-fail-or-recover
|
||||
with:
|
||||
channel: turtle-bots
|
||||
slack_webhook: ${{ secrets.SLACK_EAS_WORKFLOWS_WEBHOOK }}
|
||||
+1
-1203
File diff suppressed because it is too large.
Load diff
@@ -1,170 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
This is a **Lerna-based monorepo** with four packages:
|
||||
|
||||
1. **[packages/eas-cli](./packages/eas-cli/CLAUDE.md)** - The main CLI tool (published as `eas-cli`)
|
||||
|
||||
- 95+ commands organized by domain (build, submit, update, channel, credentials, etc.)
|
||||
- Built on oclif (Open CLI Framework)
|
||||
- Entry point: `packages/eas-cli/bin/run`
|
||||
|
||||
2. **[packages/eas-json](./packages/eas-json/CLAUDE.md)** - EAS configuration parser (published as `@expo/eas-json`)
|
||||
|
||||
- Validates `eas.json` configuration files using Joi schemas
|
||||
- Manages build, submit, and deploy profiles
|
||||
- Supports JSON5 via golden-fleece
|
||||
|
||||
3. **[packages/eas-build-cache-provider](./packages/eas-build-cache-provider/CLAUDE.md)** - Build cache plugin (published as `eas-build-cache-provider`)
|
||||
|
||||
- Optimizes build caching for Expo CLI
|
||||
|
||||
4. **[packages/worker](./packages/worker/CLAUDE.md)** - Turtle Worker service (private, not published)
|
||||
- Runs on EAS build VMs/pods
|
||||
- WebSocket server for communication with Turtle Launcher
|
||||
- Wraps `@expo/build-tools` to execute actual React Native builds
|
||||
|
||||
> **Note**: Each package has its own CLAUDE.md with package-specific guidance. Click the links above to view them.
|
||||
|
||||
## Common Development Commands
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
yarn install # Install all dependencies
|
||||
yarn build # Build all packages in the monorepo
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
yarn start # Watch mode with parallel builds across all packages
|
||||
yarn watch # Alias for start
|
||||
|
||||
# Development with relaxed TypeScript rules (allows unused variables)
|
||||
yarn start-allow-unused
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
yarn test # Run all tests across packages (Jest multi-project)
|
||||
yarn test --watch # Run tests in watch mode
|
||||
|
||||
# Run tests for specific package
|
||||
cd packages/eas-cli && yarn test
|
||||
cd packages/eas-json && yarn test
|
||||
cd packages/worker && yarn test
|
||||
```
|
||||
|
||||
### Type Checking & Linting
|
||||
|
||||
```bash
|
||||
yarn typecheck # TypeScript type check all packages
|
||||
yarn lint # ESLint across all packages
|
||||
```
|
||||
|
||||
### Running EAS CLI Locally
|
||||
|
||||
```bash
|
||||
# From repository root:
|
||||
yarn eas <command> # Run via root package script
|
||||
|
||||
# Or use the binary directly:
|
||||
packages/eas-cli/bin/run <command>
|
||||
|
||||
# Recommended: Create an alias for development
|
||||
alias easd="$(pwd)/packages/eas-cli/bin/run"
|
||||
easd build --help
|
||||
```
|
||||
|
||||
## Build System
|
||||
|
||||
Each package has independent TypeScript compilation:
|
||||
|
||||
- `eas-cli`: `src/` → `build/`
|
||||
- `eas-json`: `src/` → `build/`
|
||||
- `eas-build-cache-provider`: `src/` → `build/`
|
||||
- `worker`: `src/` → `dist/`
|
||||
|
||||
TypeScript configs:
|
||||
|
||||
- `tsconfig.json` - Base configuration (extends @tsconfig/node18)
|
||||
- `tsconfig.build.json` - Production builds
|
||||
- `tsconfig.allowUnused.json` - Development with relaxed rules
|
||||
|
||||
## Testing Architecture
|
||||
|
||||
- **Framework**: Jest with multi-project configuration
|
||||
- **Root config**: `jest.config.ts` (combines eas-cli, eas-json, worker)
|
||||
- **Test locations**: `__tests__/` directories alongside source files
|
||||
- **Mocking**:
|
||||
- File system: `memfs`
|
||||
- HTTP: `nock`
|
||||
- Utilities: `ts-mockito`, `mockdate`
|
||||
|
||||
Run a single test file:
|
||||
|
||||
```bash
|
||||
yarn test <path-to-test-file>
|
||||
# Example:
|
||||
yarn test packages/eas-cli/src/project/__tests__/projectUtils-test.ts
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# API Configuration
|
||||
export EXPO_STAGING=1 # Use staging API (https://staging-api.expo.dev)
|
||||
export EXPO_LOCAL=1 # Use local API (http://127.0.0.1:3000)
|
||||
|
||||
# Development
|
||||
export EAS_NO_VCS=1 # Disable version control checks
|
||||
export EXPO_DEBUG=1 # Enable debug logging
|
||||
```
|
||||
|
||||
## Code Style & Conventions
|
||||
|
||||
### Validation
|
||||
|
||||
All changes should be validated with TypeScript and the linter before committing:
|
||||
|
||||
```bash
|
||||
yarn typecheck # Validate TypeScript types
|
||||
yarn lint # Run ESLint
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
Use the `Log` module from `@expo/logger`:
|
||||
|
||||
```typescript
|
||||
import { Log } from '@expo/logger';
|
||||
|
||||
Log.log('Info message');
|
||||
Log.warn('Warning message');
|
||||
Log.error('Error message');
|
||||
```
|
||||
|
||||
### Pull Request conventions
|
||||
|
||||
When creating pull requests, make sure to adhere to [PULL_REQUEST_TEMPLATE](./.github/PULL_REQUEST_TEMPLATE).
|
||||
|
||||
## Release Process
|
||||
|
||||
1. Update `CHANGELOG.md` in the appropriate package
|
||||
2. Version bump is automated based on changelog:
|
||||
- Breaking changes → MAJOR
|
||||
- New features → MINOR
|
||||
- Otherwise → PATCH
|
||||
3. GitHub Actions workflow handles release automation
|
||||
4. Notifications sent to Slack #eas-cli channel
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Node Version**: Requires Node.js >= 18.0.0 (managed via Volta)
|
||||
- **Package Manager**: Uses Yarn 1.22.21
|
||||
- **Compilation Target**: CommonJS with Node resolution
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
* @szdziedzic @khamilowicz @sjchmiela
|
||||
|
||||
packages/eas-cli/src/build @szdziedzic @khamilowicz @sjchmiela
|
||||
packages/eas-cli/src/commands/build @szdziedzic @khamilowicz @sjchmiela
|
||||
|
||||
packages/eas-cli/src/submit @szdziedzic @khamilowicz @sjchmiela
|
||||
packages/eas-cli/src/commands/submit.ts @szdziedzic @khamilowicz @sjchmiela
|
||||
|
||||
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 @sjchmiela
|
||||
+5
-14
@@ -1,21 +1,12 @@
|
||||
# Development
|
||||
|
||||
## Getting Started
|
||||
Set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
If this is your first time running `eas-cli` locally, make sure you install and build the packages. In the project root, run:
|
||||
|
||||
```sh
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
From there, set up an alias for the EAS CLI so you can try it in projects all around your computer. The project is compiled on the fly so you don't need to run a build (watch) command.
|
||||
|
||||
> The only exception is if there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
> The only exception is when you just cloned the repository or there have been changes in `packages/eas-json`. In that case, you'll have to run `yarn build` in the root.
|
||||
|
||||
Open your `.zshrc` or other config file and add:
|
||||
|
||||
```sh
|
||||
```
|
||||
alias easd="/PATH/TO/eas-cli/bin/run"
|
||||
```
|
||||
|
||||
@@ -23,7 +14,7 @@ Then use it with `easd` like `easd build`.
|
||||
|
||||
If you're making changes to `packages/eas-json` or prefer/need to work with production code, start the build command in watch mode with:
|
||||
|
||||
```sh
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
|
||||
@@ -42,7 +33,7 @@ If it is easier for you to work with the code locally with compiler options set
|
||||
|
||||
you can consider using:
|
||||
|
||||
```sh
|
||||
```
|
||||
yarn start-allow-unused
|
||||
```
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Releasing EAS CLI
|
||||
|
||||
1. Invoke the GitHub workflow ["Manually trigger a release"](https://github.com/expo/eas-cli/actions/workflows/trigger-release.yml). The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as an input to the workflow, in semver format, e.g. "1.2.3"
|
||||
1. Run `yarn release` in the repository root folder. The next version is chosen automatically based on the changelog entries. If you want to use different version, pass the version string as a positional argument to the command, e.g. `yarn release 1.2.3`.
|
||||
2. That's it! GitHub Actions is going to take care of the rest. Watch the #eas-cli Slack channel for a successful release notification.
|
||||
|
||||
## Choosing the next version
|
||||
|
||||
+1
-3
@@ -4,11 +4,9 @@ import type { Config } from 'jest';
|
||||
import easCliConfig from './packages/eas-cli/jest.config';
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import easJsonConfig from './packages/eas-json/jest.config';
|
||||
// eslint-disable-next-line import/no-relative-packages
|
||||
import workerConfig from './packages/worker/jest.config.unit';
|
||||
|
||||
const config: Config = {
|
||||
projects: [easCliConfig, easJsonConfig, workerConfig],
|
||||
projects: [easCliConfig, easJsonConfig],
|
||||
testPathIgnorePatterns: ['.*'],
|
||||
watchPlugins: ['jest-watch-typeahead/filename', 'jest-watch-typeahead/testname'],
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.{ts,tsx,js,jsx}'],
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "16.30.0",
|
||||
"version": "7.8.2",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+5
-5
@@ -21,21 +21,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.14.8",
|
||||
"@types/node": "20.11.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-node": "11.1.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-watch-typeahead": "2.2.2",
|
||||
"lerna": "9.0.3",
|
||||
"lerna": "7.4.2",
|
||||
"oclif": "^3.9.0",
|
||||
"prettier": "3.1.1",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.5.4"
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.19.4",
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
# CLAUDE.md - eas-build-cache-provider
|
||||
|
||||
This file provides guidance for working with the **eas-build-cache-provider** package.
|
||||
|
||||
## Overview
|
||||
|
||||
A build cache provider plugin for Expo CLI. Published as `eas-build-cache-provider` on npm.
|
||||
|
||||
- Provides remote caching for Expo builds
|
||||
- Optimizes build times by caching intermediate build artifacts
|
||||
- Integrates with EAS infrastructure
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
yarn build # TypeScript compilation (src → build)
|
||||
yarn watch # Watch mode
|
||||
yarn typecheck # Type check
|
||||
yarn test # Run tests
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Install as a dev dependency in your Expo project:
|
||||
|
||||
```bash
|
||||
npm install --save-dev eas-build-cache-provider
|
||||
# or
|
||||
yarn add --dev eas-build-cache-provider
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Update your **app.json** to enable the EAS build cache provider:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
"experiments": {
|
||||
"buildCacheProvider": "eas"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
The build cache provider:
|
||||
1. Hooks into Expo CLI's build process
|
||||
2. Uploads build cache artifacts to EAS infrastructure
|
||||
3. Downloads cached artifacts on subsequent builds
|
||||
4. Improves build times by reusing cached dependencies and build outputs
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Experimental**: This is an experimental feature
|
||||
- **EAS Integration**: Requires EAS account and project setup
|
||||
- **Cache Scope**: Cache is scoped per project and platform
|
||||
@@ -1,22 +0,0 @@
|
||||
# eas-build-cache-provider
|
||||
|
||||
A build cache provider plugin for the Expo CLI
|
||||
|
||||
To use the EAS remote build provider plugin, install the `eas-build-cache-provider` package as a developer dependency:
|
||||
|
||||
```bash
|
||||
npm install --save-dev eas-build-cache-provider
|
||||
```
|
||||
|
||||
Then, update your **app.json** to include the `buildCacheProvider` property and its provider under `experiments`:
|
||||
|
||||
```json
|
||||
{
|
||||
"expo": {
|
||||
...
|
||||
"experiments": {
|
||||
"buildCacheProvider": "eas"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"name": "eas-build-cache-provider",
|
||||
"description": "A build cache provider plugin for the Expo CLI",
|
||||
"version": "16.30.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "7.23.5",
|
||||
"@expo/config": "11.0.10",
|
||||
"@expo/spawn-async": "^1.7.2",
|
||||
"chalk": "4.1.2",
|
||||
"figures": "3.2.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"getenv": "1.0.0",
|
||||
"log-symbols": "4.1.0",
|
||||
"semver": "7.5.2",
|
||||
"terminal-link": "2.1.1",
|
||||
"tslib": "2.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/babel__code-frame": "7.0.3",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/expo/eas-cli",
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/eas-build-cache-provider"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
module.exports = {
|
||||
extends: ['universe/shared/typescript-analysis'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.ts', '*.d.ts'],
|
||||
parserOptions: {
|
||||
project: './packages/eas-build-cache-provider/tsconfig.json',
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-function-return-type': [
|
||||
'warn',
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
import { RunOptions, getPackageJson } from '@expo/config';
|
||||
import { SpawnResult } from '@expo/spawn-async';
|
||||
|
||||
export function isSpawnResultError(obj: any): obj is Error & SpawnResult {
|
||||
return (
|
||||
obj &&
|
||||
'message' in obj &&
|
||||
obj.status !== undefined &&
|
||||
obj.stdout !== undefined &&
|
||||
obj.stderr !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
export function isDevClientBuild({
|
||||
runOptions,
|
||||
projectRoot,
|
||||
}: {
|
||||
runOptions: RunOptions;
|
||||
projectRoot: string;
|
||||
}): boolean {
|
||||
if (!hasDirectDevClientDependency(projectRoot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ('variant' in runOptions && runOptions.variant !== undefined) {
|
||||
return runOptions.variant === 'debug';
|
||||
}
|
||||
if ('configuration' in runOptions && runOptions.configuration !== undefined) {
|
||||
return runOptions.configuration === 'Debug';
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function hasDirectDevClientDependency(projectRoot: string): boolean {
|
||||
const { dependencies = {}, devDependencies = {} } = getPackageJson(projectRoot);
|
||||
return !!dependencies['expo-dev-client'] || !!devDependencies['expo-dev-client'];
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
import {
|
||||
BuildCacheProviderPlugin,
|
||||
CalculateFingerprintHashProps,
|
||||
ResolveBuildCacheProps,
|
||||
UploadBuildCacheProps,
|
||||
} from '@expo/config';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { isDevClientBuild, isSpawnResultError } from './helpers';
|
||||
import Log from './log';
|
||||
|
||||
async function resolveBuildCacheAsync({
|
||||
projectRoot,
|
||||
platform,
|
||||
fingerprintHash,
|
||||
runOptions,
|
||||
}: ResolveBuildCacheProps): Promise<string | null> {
|
||||
const easJsonPath = path.join(projectRoot, 'eas.json');
|
||||
if (!(await fs.exists(easJsonPath))) {
|
||||
Log.debug('eas.json not found, skip checking for remote builds');
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.log(
|
||||
chalk`{whiteBright \u203A} {bold Searching builds with matching fingerprint on EAS servers}`
|
||||
);
|
||||
try {
|
||||
const results = await spawnAsync(
|
||||
'npx',
|
||||
[
|
||||
'eas-cli',
|
||||
'build:download',
|
||||
`--platform=${platform}`,
|
||||
`--fingerprint=${fingerprintHash}`,
|
||||
'--non-interactive',
|
||||
isDevClientBuild({ runOptions, projectRoot }) ? '--dev-client' : '--no-dev-client',
|
||||
'--json',
|
||||
],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
}
|
||||
);
|
||||
|
||||
Log.log(chalk`{whiteBright \u203A} {bold Successfully downloaded cached build}`);
|
||||
// {
|
||||
// "path": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
|
||||
// }
|
||||
const json = JSON.parse(results.stdout.trim());
|
||||
return json?.path;
|
||||
} catch (error) {
|
||||
Log.debug('eas-cli error:', error);
|
||||
// @TODO(2025-04-11): remove this in a future release
|
||||
if (isSpawnResultError(error) && error.stderr.includes('command build:download not found')) {
|
||||
Log.warn(
|
||||
`To take advantage of EAS build cache provider, upgrade your eas-cli installation to latest.`
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadBuildCacheAsync({
|
||||
projectRoot,
|
||||
platform,
|
||||
fingerprintHash,
|
||||
buildPath,
|
||||
}: UploadBuildCacheProps): Promise<string | null> {
|
||||
const easJsonPath = path.join(projectRoot, 'eas.json');
|
||||
if (!(await fs.exists(easJsonPath))) {
|
||||
Log.debug('eas.json not found, skip uploading builds');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.log(chalk`{whiteBright \u203A} {bold Uploading build to EAS}`);
|
||||
const results = await spawnAsync(
|
||||
'npx',
|
||||
[
|
||||
'eas-cli',
|
||||
'upload',
|
||||
`--platform=${platform}`,
|
||||
`--fingerprint=${fingerprintHash}`,
|
||||
buildPath ? `--build-path=${buildPath}` : '',
|
||||
'--non-interactive',
|
||||
'--json',
|
||||
],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
}
|
||||
);
|
||||
// {
|
||||
// "url": "/var/folders/03/lppcpcnn61q3mz5ckzmzd8w80000gn/T/eas-cli-nodejs/eas-build-run-cache/c0f9ba9c-0cf1-4c5c-8566-b28b7971050f_22f1bbfa-1c09-4b67-9e4a-721906546b58.app"
|
||||
// }
|
||||
const json = JSON.parse(results.stdout.trim());
|
||||
Log.log(chalk`{whiteBright \u203A} {bold Build successfully uploaded: ${json?.url}}`);
|
||||
return json?.url;
|
||||
} catch (error) {
|
||||
Log.debug('eas-cli error:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function calculateEASFingerprintHashAsync({
|
||||
projectRoot,
|
||||
platform,
|
||||
}: CalculateFingerprintHashProps): Promise<string | null> {
|
||||
// prefer using `eas fingerprint:generate` because it automatically upload sources
|
||||
try {
|
||||
const results = await spawnAsync(
|
||||
'npx',
|
||||
['eas-cli', 'fingerprint:generate', `--platform=${platform}`, '--json', '--non-interactive'],
|
||||
{
|
||||
cwd: projectRoot,
|
||||
}
|
||||
);
|
||||
// {
|
||||
// "hash": "203f960b965e154b77dc31c6c42e5582e8d77196"
|
||||
// }
|
||||
const json = JSON.parse(results.stdout.trim());
|
||||
return json?.hash;
|
||||
} catch (error) {
|
||||
Log.debug('eas-cli error:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const EASBuildCacheProvider: BuildCacheProviderPlugin = {
|
||||
resolveBuildCache: resolveBuildCacheAsync,
|
||||
uploadBuildCache: uploadBuildCacheAsync,
|
||||
calculateFingerprintHash: calculateEASFingerprintHashAsync,
|
||||
};
|
||||
|
||||
export default EASBuildCacheProvider;
|
||||
@@ -1,121 +0,0 @@
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import { boolish } from 'getenv';
|
||||
import logSymbols from 'log-symbols';
|
||||
import terminalLink from 'terminal-link';
|
||||
|
||||
type Color = (...text: string[]) => string;
|
||||
|
||||
export default class Log {
|
||||
public static readonly isDebug = boolish('EXPO_DEBUG', false);
|
||||
|
||||
public static log(...args: any[]): void {
|
||||
Log.consoleLog(...args);
|
||||
}
|
||||
|
||||
public static newLine(): void {
|
||||
Log.consoleLog();
|
||||
}
|
||||
|
||||
public static addNewLineIfNone(): void {
|
||||
if (!Log.isLastLineNewLine) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
public static error(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.red));
|
||||
}
|
||||
|
||||
public static warn(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.yellow));
|
||||
}
|
||||
|
||||
public static debug(...args: any[]): void {
|
||||
if (Log.isDebug) {
|
||||
Log.consoleLog(...args);
|
||||
}
|
||||
}
|
||||
|
||||
public static gray(...args: any[]): void {
|
||||
Log.consoleLog(...Log.withTextColor(args, chalk.gray));
|
||||
}
|
||||
|
||||
public static warnDeprecatedFlag(flag: string, message: string): void {
|
||||
Log.warn(`› ${chalk.bold('--' + flag)} flag is deprecated. ${message}`);
|
||||
}
|
||||
|
||||
public static fail(message: string): void {
|
||||
Log.log(`${chalk.red(logSymbols.error)} ${message}`);
|
||||
}
|
||||
|
||||
public static succeed(message: string): void {
|
||||
Log.log(`${chalk.green(logSymbols.success)} ${message}`);
|
||||
}
|
||||
|
||||
public static withTick(...args: any[]): void {
|
||||
Log.consoleLog(chalk.green(figures.tick), ...args);
|
||||
}
|
||||
|
||||
public static withInfo(...args: any[]): void {
|
||||
Log.consoleLog(chalk.green(figures.info), ...args);
|
||||
}
|
||||
|
||||
private static consoleLog(...args: any[]): void {
|
||||
Log.updateIsLastLineNewLine(args);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(...args);
|
||||
}
|
||||
|
||||
private static withTextColor(args: any[], chalkColor: Color): string[] {
|
||||
return args.map(arg => chalkColor(arg));
|
||||
}
|
||||
|
||||
private static isLastLineNewLine = false;
|
||||
private static updateIsLastLineNewLine(args: any[]): void {
|
||||
if (args.length === 0) {
|
||||
Log.isLastLineNewLine = true;
|
||||
} else {
|
||||
const lastArg = args[args.length - 1];
|
||||
if (typeof lastArg === 'string' && (lastArg === '' || lastArg.match(/[\r\n]$/))) {
|
||||
Log.isLastLineNewLine = true;
|
||||
} else {
|
||||
Log.isLastLineNewLine = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints a link for given URL, using text if provided, otherwise text is just the URL.
|
||||
* Format links as dim (unless disabled) and with an underline.
|
||||
*
|
||||
* @example https://expo.dev
|
||||
*/
|
||||
export function link(
|
||||
url: string,
|
||||
{ text = url, fallback, dim = true }: { text?: string; dim?: boolean; fallback?: string } = {}
|
||||
): string {
|
||||
// Links can be disabled via env variables https://github.com/jamestalmage/supports-hyperlinks/blob/master/index.js
|
||||
const output = terminalLink(text, url, {
|
||||
fallback: () =>
|
||||
fallback ?? (text === url ? chalk.underline(url) : `${text}: ${chalk.underline(url)}`),
|
||||
});
|
||||
return dim ? chalk.dim(output) : output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a consistent "Learn more" link experience.
|
||||
* Format links as dim (unless disabled) with an underline.
|
||||
*
|
||||
* @example Learn more: https://expo.dev
|
||||
*/
|
||||
export function learnMore(
|
||||
url: string,
|
||||
{
|
||||
learnMoreMessage: maybeLearnMoreMessage,
|
||||
dim = true,
|
||||
}: { learnMoreMessage?: string; dim?: boolean } = {}
|
||||
): string {
|
||||
return link(url, { text: maybeLearnMoreMessage ?? 'Learn more', dim });
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"exclude": ["**/__mocks__/*", "**/__tests__/*"]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"extends": "@tsconfig/node18/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"importHelpers": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"outDir": "build",
|
||||
"rootDir": "src",
|
||||
"typeRoots": ["../../node_modules/@types", "../../ts-declarations", "./node_modules/@types"]
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
# CLAUDE.md - eas-cli
|
||||
|
||||
This file provides guidance for working with the **eas-cli** package.
|
||||
|
||||
## Overview
|
||||
|
||||
The main CLI tool for Expo Application Services (EAS). Published as `eas-cli` on npm.
|
||||
|
||||
- **95+ commands** organized by domain
|
||||
- Built on **oclif** (Open CLI Framework)
|
||||
- Entry point: `bin/run`
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Running Locally
|
||||
```bash
|
||||
# From repository root:
|
||||
yarn eas <command>
|
||||
|
||||
# Or use the binary directly:
|
||||
packages/eas-cli/bin/run <command>
|
||||
|
||||
# Recommended alias for development:
|
||||
alias easd="$(pwd)/packages/eas-cli/bin/run"
|
||||
easd build --help
|
||||
```
|
||||
|
||||
### Build & Development
|
||||
```bash
|
||||
yarn build # TypeScript compilation (src → build)
|
||||
yarn watch # Watch mode
|
||||
yarn watch-allow-unused # Watch mode with relaxed TS rules
|
||||
yarn typecheck # Type check without building
|
||||
yarn typecheck-for-build # Type check for production build
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
yarn test # Run all tests
|
||||
yarn test <path-to-file> # Run specific test file
|
||||
```
|
||||
|
||||
### GraphQL Code Generation
|
||||
```bash
|
||||
yarn generate-graphql-code # Generate TypeScript types from GraphQL schema
|
||||
yarn verify-graphql-code # Verify GraphQL code is up to date (CI check)
|
||||
```
|
||||
|
||||
The GraphQL schema is fetched from `https://staging-api.expo.dev/graphql` and generates:
|
||||
- `src/graphql/generated.ts` - 499KB+ of TypeScript types
|
||||
- Types for all queries, mutations, and fragments
|
||||
|
||||
### oclif Commands
|
||||
```bash
|
||||
yarn oclif readme # Auto-generate README.md from command metadata
|
||||
```
|
||||
|
||||
## Command Architecture
|
||||
|
||||
### Base Command Class
|
||||
|
||||
All commands extend `EasCommand` (in `src/commandUtils/EasCommand.ts`).
|
||||
|
||||
### Context System
|
||||
|
||||
Commands declare required context via `ContextOptions`. This is a powerful system that provides commands with pre-initialized resources based on their needs.
|
||||
|
||||
Available context types:
|
||||
|
||||
- **LoggedIn**: Requires authentication
|
||||
- Provides: `actor` (user info), `graphqlClient` (authenticated GraphQL client)
|
||||
- **MaybeLoggedIn**: Optional authentication
|
||||
- Provides: `actor` (may be undefined), `graphqlClient`
|
||||
- **ProjectConfig**: Requires project with app.json/app.config
|
||||
- Provides: `projectId`, `exp` (Expo config), project directory
|
||||
- **ProjectDir**: Project directory context
|
||||
- Provides: `projectDir`, `projectRoot`
|
||||
- **VcsClient**: Version control system integration
|
||||
- Provides: `vcsClient` (Git operations)
|
||||
- **ServerSideEnvironmentVariables**: Environment variable management
|
||||
- Provides: methods to interact with server-side env vars
|
||||
|
||||
Example command structure:
|
||||
```typescript
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
|
||||
export default class Build extends EasCommand {
|
||||
static override description = 'start a build';
|
||||
|
||||
static override ContextOptions = {
|
||||
...EasCommand.ContextOptions.LoggedIn, // Requires auth
|
||||
...EasCommand.ContextOptions.ProjectConfig, // Requires project
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
// Context is automatically initialized
|
||||
const { graphqlClient } = await this.getContextAsync(LoggedIn, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const { projectId, exp } = await this.getContextAsync(ProjectConfig, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
// Your command logic here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Command Discovery
|
||||
|
||||
- **Location**: Commands are auto-discovered from `build/commands/` by oclif
|
||||
- **Manifest**: `oclif.manifest.json` is generated during `prepack`
|
||||
- **Topics**: Organized into topics (account, build, channel, update, etc.)
|
||||
|
||||
## API Architecture
|
||||
|
||||
### GraphQL API (Primary)
|
||||
|
||||
**Location**: `src/graphql/`
|
||||
|
||||
**Client**: `ExpoGraphqlClient` (urql-based wrapper)
|
||||
- Automatic retry logic with exponential backoff
|
||||
- Cache management (cache-first vs network-only)
|
||||
- Transient error handling
|
||||
- Authentication via Bearer token or session secret
|
||||
|
||||
**Structure**:
|
||||
```
|
||||
src/graphql/
|
||||
├── generated.ts # Generated TypeScript types
|
||||
├── queries/ # 24+ query modules
|
||||
├── mutations/ # 17+ mutation modules
|
||||
└── types/ # GraphQL fragments
|
||||
```
|
||||
|
||||
**Making GraphQL Requests**:
|
||||
```typescript
|
||||
// In a command with LoggedIn context:
|
||||
const { graphqlClient } = await this.getContextAsync(LoggedIn, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
const result = await graphqlClient
|
||||
.query(MyQuery, { variables: { id: projectId } })
|
||||
.toPromise();
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error.message);
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
```
|
||||
|
||||
### REST API v2 (Legacy)
|
||||
|
||||
**Client**: `ApiV2Client` in `src/api.ts`
|
||||
|
||||
**Base URLs**:
|
||||
- Production: `https://api.expo.dev`
|
||||
- Staging: `https://staging-api.expo.dev` (set `EXPO_STAGING=1`)
|
||||
- Local: `http://127.0.0.1:3000` (set `EXPO_LOCAL=1`)
|
||||
|
||||
**Methods**: GET, POST, PUT, DELETE
|
||||
|
||||
**Error Handling**: `ApiV2Error` wrapper
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── commands/ # CLI command implementations (organized by domain)
|
||||
│ ├── account/ # Login, logout, account management
|
||||
│ ├── build/ # Build orchestration commands
|
||||
│ ├── branch/ # Update branch management
|
||||
│ ├── channel/ # Update channel management
|
||||
│ ├── credentials/ # iOS/Android credential management
|
||||
│ ├── deploy/ # Deployment commands
|
||||
│ ├── device/ # Apple device management
|
||||
│ ├── env/ # Environment variable management
|
||||
│ ├── fingerprint/ # Fingerprint comparison/generation
|
||||
│ ├── metadata/ # App store metadata management
|
||||
│ ├── project/ # Project initialization and info
|
||||
│ ├── secret/ # Secret management (legacy)
|
||||
│ ├── submit/ # App store submission
|
||||
│ ├── update/ # EAS Update management
|
||||
│ ├── webhook/ # Webhook management
|
||||
│ └── workflow/ # Workflow management
|
||||
├── commandUtils/ # Base command classes and shared utilities
|
||||
├── build/ # Build orchestration logic
|
||||
├── credentials/ # Credential management system
|
||||
├── project/ # Project configuration and publishing
|
||||
├── update/ # EAS Update functionality
|
||||
├── submit/ # App store submission logic
|
||||
├── user/ # Authentication and user management
|
||||
├── graphql/ # GraphQL queries, mutations, and types
|
||||
├── analytics/ # Rudder Analytics integration
|
||||
├── vcs/ # Version control abstraction (Git)
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Working with eas.json
|
||||
```typescript
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
|
||||
// Read eas.json
|
||||
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
const easJson = await EasJsonUtils.readAsync(projectDir);
|
||||
|
||||
// Access build profiles
|
||||
const buildProfile = easJson.build?.[profileName];
|
||||
```
|
||||
|
||||
### Project Context
|
||||
```typescript
|
||||
// In a command with ProjectConfig context:
|
||||
const { projectId, exp } = await this.getContextAsync(ProjectConfig, {
|
||||
nonInteractive: false,
|
||||
});
|
||||
|
||||
// projectId: EAS project ID
|
||||
// exp: Parsed Expo config (app.json/app.config.js)
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. **SessionManager** handles login/logout (`src/user/SessionManager.ts`)
|
||||
2. Authentication methods:
|
||||
- Interactive login
|
||||
- SSO via browser launcher
|
||||
- Session secrets
|
||||
- Access tokens
|
||||
3. Credentials stored via `UserSettings`
|
||||
|
||||
### Feature Flags
|
||||
|
||||
- Server-side feature gating via `FeatureGating` class
|
||||
- Environment variable overrides via `FeatureGateEnvOverrides`
|
||||
- Retrieved from Actor during authentication
|
||||
|
||||
### Error Handling
|
||||
|
||||
Extend `CommandError` or use specific error classes in `src/commandUtils/errors.ts`:
|
||||
```typescript
|
||||
import { CommandError } from '../commandUtils/errors';
|
||||
|
||||
throw new CommandError('BUILD_FAILED', 'The build failed due to...');
|
||||
```
|
||||
|
||||
### Analytics
|
||||
|
||||
- **Provider**: Rudder Analytics (`@expo/rudder-sdk-node`)
|
||||
- Events tracked for builds, submissions, commands
|
||||
- User can opt-out via settings
|
||||
|
||||
## Testing
|
||||
|
||||
- **Location**: `__tests__/` directories alongside source files
|
||||
- **Mocking**:
|
||||
- File system: `memfs`
|
||||
- HTTP: `nock`
|
||||
- Utilities: `ts-mockito`, `mockdate`
|
||||
|
||||
Example test:
|
||||
```typescript
|
||||
import { vol } from 'memfs';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('MyCommand', () => {
|
||||
beforeEach(() => {
|
||||
vol.reset();
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
it('should do something', async () => {
|
||||
// Test implementation
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **oclif**: Commands auto-discovered from `build/commands/`
|
||||
- **README**: Auto-generated from oclif - update via `yarn oclif readme`
|
||||
- **Manifest**: Generated during `prepack` step
|
||||
- **Templates**: `src/commandUtils/new/templates` are copied to `build/` during build
|
||||
- **Node Version**: >= 18.0.0
|
||||
+176
-1150
File diff suppressed because it is too large.
Load diff
@@ -12,6 +12,3 @@ if (process.env.TMPDIR) {
|
||||
(fs.realpath as any).native = jest.fn();
|
||||
|
||||
module.exports = fs;
|
||||
|
||||
// Ensure requiring node:fs returns the mock too. This is needed for expo/config and expo/json-file.
|
||||
jest.mock('node:fs', () => fs);
|
||||
@@ -1,7 +1,12 @@
|
||||
overwrite: true
|
||||
schema: 'https://staging-api.expo.dev/graphql'
|
||||
documents:
|
||||
- 'src/**/!(*.d).ts'
|
||||
- 'src/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/ios/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/credentials/android/api/graphql/**/!(*.d).{ts,tsx}'
|
||||
- 'src/commands/**/*.ts'
|
||||
- 'src/branch/**/*.ts'
|
||||
- 'src/channel/**/*.ts'
|
||||
generates:
|
||||
src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
||||
Generated
+2369
-27457
File diff suppressed because it is too large.
Load diff
@@ -1,34 +1,34 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "16.30.0",
|
||||
"version": "7.8.2",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.13",
|
||||
"@expo/apple-utils": "1.7.0",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.243",
|
||||
"@expo/eas-json": "16.30.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "1.0.221",
|
||||
"@expo/multipart-body-parser": "2.0.0",
|
||||
"@expo/osascript": "2.1.4",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/pkcs12": "0.1.3",
|
||||
"@expo/plist": "0.2.0",
|
||||
"@expo/config": "8.5.4",
|
||||
"@expo/config-plugins": "7.8.4",
|
||||
"@expo/config-types": "50.0.0",
|
||||
"@expo/eas-build-job": "1.0.95",
|
||||
"@expo/eas-json": "7.8.0",
|
||||
"@expo/json-file": "8.2.37",
|
||||
"@expo/logger": "1.0.57",
|
||||
"@expo/multipart-body-parser": "1.1.0",
|
||||
"@expo/osascript": "2.0.33",
|
||||
"@expo/package-manager": "1.1.2",
|
||||
"@expo/pkcs12": "0.0.8",
|
||||
"@expo/plist": "0.0.20",
|
||||
"@expo/plugin-help": "5.1.23",
|
||||
"@expo/plugin-warn-if-update-available": "2.5.1",
|
||||
"@expo/prebuild-config": "8.0.17",
|
||||
"@expo/prebuild-config": "6.7.3",
|
||||
"@expo/results": "1.0.0",
|
||||
"@expo/rudder-sdk-node": "1.1.1",
|
||||
"@expo/spawn-async": "1.7.2",
|
||||
"@expo/steps": "1.0.231",
|
||||
"@expo/spawn-async": "1.7.0",
|
||||
"@expo/steps": "1.0.95",
|
||||
"@expo/timeago.js": "1.0.0",
|
||||
"@oclif/core": "^1.26.2",
|
||||
"@oclif/plugin-autocomplete": "^2.3.10",
|
||||
@@ -38,18 +38,16 @@
|
||||
"ajv": "8.11.0",
|
||||
"ajv-formats": "2.1.1",
|
||||
"better-opn": "3.0.2",
|
||||
"bplist-parser": "^0.3.0",
|
||||
"chalk": "4.1.2",
|
||||
"cli-progress": "3.12.0",
|
||||
"dateformat": "4.6.3",
|
||||
"diff": "7.0.0",
|
||||
"dotenv": "16.3.1",
|
||||
"env-paths": "2.2.0",
|
||||
"envinfo": "7.11.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"fast-glob": "3.3.2",
|
||||
"figures": "3.2.0",
|
||||
"form-data": "^4.0.4",
|
||||
"form-data": "4.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"getenv": "1.0.0",
|
||||
"gradle-to-js": "2.0.1",
|
||||
@@ -58,7 +56,6 @@
|
||||
"https-proxy-agent": "5.0.1",
|
||||
"ignore": "5.3.0",
|
||||
"indent-string": "4.0.0",
|
||||
"invariant": "^2.2.2",
|
||||
"jks-js": "1.1.0",
|
||||
"joi": "17.11.0",
|
||||
"jsonwebtoken": "9.0.0",
|
||||
@@ -66,11 +63,9 @@
|
||||
"log-symbols": "4.1.0",
|
||||
"mime": "3.0.0",
|
||||
"minimatch": "5.1.2",
|
||||
"minizlib": "3.0.1",
|
||||
"nanoid": "3.3.8",
|
||||
"nanoid": "3.3.4",
|
||||
"node-fetch": "2.6.7",
|
||||
"node-forge": "1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
@@ -81,60 +76,52 @@
|
||||
"qrcode-terminal": "0.12.0",
|
||||
"resolve-from": "5.0.0",
|
||||
"semver": "7.5.4",
|
||||
"set-interval-async": "3.0.3",
|
||||
"slash": "3.0.0",
|
||||
"tar": "6.2.1",
|
||||
"tar-stream": "3.1.7",
|
||||
"terminal-link": "2.1.1",
|
||||
"ts-deepmerge": "6.2.0",
|
||||
"tslib": "2.6.2",
|
||||
"turndown": "7.1.2",
|
||||
"untildify": "4.0.0",
|
||||
"uuid": "9.0.1",
|
||||
"wrap-ansi": "7.0.0",
|
||||
"yaml": "2.6.0",
|
||||
"zod": "^4.1.3"
|
||||
"wrap-ansi": "7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/introspection": "4.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@tsconfig/node18": "18.2.4",
|
||||
"@types/cli-progress": "3.11.5",
|
||||
"@types/dateformat": "3.0.1",
|
||||
"@types/diff": "6.0.0",
|
||||
"@types/envinfo": "7.8.3",
|
||||
"@types/express": "4.17.15",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/getenv": "^1.0.0",
|
||||
"@types/invariant": "^2.2.37",
|
||||
"@types/jsonwebtoken": "8.5.1",
|
||||
"@types/mime": "3.0.1",
|
||||
"@types/node-fetch": "2.6.12",
|
||||
"@types/node-fetch": "2.6.2",
|
||||
"@types/node-forge": "1.3.1",
|
||||
"@types/pngjs": "6.0.4",
|
||||
"@types/promise-retry": "1.1.3",
|
||||
"@types/prompts": "2.4.2",
|
||||
"@types/semver": "7.5.6",
|
||||
"@types/tar": "6.1.10",
|
||||
"@types/tar-stream": "3.1.3",
|
||||
"@types/tough-cookie": "4.0.2",
|
||||
"@types/uuid": "9.0.7",
|
||||
"@types/wrap-ansi": "3.0.0",
|
||||
"axios": "1.11.0",
|
||||
"axios": "0.27.2",
|
||||
"eslint-plugin-graphql": "4.0.0",
|
||||
"express": "4.20.0",
|
||||
"express": "4.19.2",
|
||||
"memfs": "3.4.13",
|
||||
"mockdate": "3.0.5",
|
||||
"nock": "13.4.0",
|
||||
"rimraf": "3.0.2",
|
||||
"ts-deepmerge": "6.2.0",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.5.4"
|
||||
"typescript": "5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
@@ -185,22 +172,16 @@
|
||||
"secret": {
|
||||
"description": "manage project and account secrets"
|
||||
},
|
||||
"env": {
|
||||
"description": "manage project and account environment variables"
|
||||
},
|
||||
"update": {
|
||||
"description": "manage individual updates"
|
||||
},
|
||||
"webhook": {
|
||||
"description": "manage webhooks"
|
||||
},
|
||||
"workflow": {
|
||||
"description": "manage workflows"
|
||||
}
|
||||
},
|
||||
"warn-if-update-available": {
|
||||
"timeoutInDays": 0.5,
|
||||
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run:') %>\n<%= chalk.dim(chalk.bold('npm install -g eas-cli')) %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
|
||||
"message": "<%= chalk('★') %> <%= chalk.bold(config.name + '@' + latest) %> <%= chalk.bold('is now available.') %>\n<%= chalk.dim('To upgrade, run ') %><%= chalk.dim(chalk.bold('npm install -g eas-cli')) %><%= chalk.dim('.') %>\n<%= chalk.dim('Proceeding with outdated version.') %>\n"
|
||||
},
|
||||
"update": {
|
||||
"node": {
|
||||
@@ -222,27 +203,24 @@
|
||||
"-v"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/eas-cli"
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"scripts": {
|
||||
"postpack": "rimraf oclif.manifest.json",
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "yarn typecheck-for-build && yarn copy-new-templates",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
||||
"watch": "yarn typecheck-for-build --watch --preserveWatchOutput",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
|
||||
"typecheck": "tsc",
|
||||
"typecheck-for-build": "tsc --project tsconfig.build.json",
|
||||
"test": "jest",
|
||||
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
|
||||
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
|
||||
"verify-graphql-code": "./scripts/verify-graphql.sh",
|
||||
"clean": "rimraf dist build tmp node_modules yarn-error.log",
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
"clean": "rimraf dist build tmp node_modules yarn-error.log"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-06/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
],
|
||||
"definitions": {
|
||||
"apple": {
|
||||
"AppleKidsAge": {
|
||||
@@ -337,8 +323,7 @@
|
||||
"gambling",
|
||||
"unrestrictedWebAccess",
|
||||
"kidsAgeBand",
|
||||
"ageRatingOverride",
|
||||
"koreaAgeRatingOverride"
|
||||
"seventeenPlus"
|
||||
],
|
||||
"defaultSnippets": [
|
||||
{
|
||||
@@ -360,9 +345,7 @@
|
||||
"gambling": false,
|
||||
"unrestrictedWebAccess": false,
|
||||
"kidsAgeBand": null,
|
||||
"ageRatingOverride": "NONE",
|
||||
"koreaAgeRatingOverride": "NONE",
|
||||
"lootBox": false
|
||||
"seventeenPlus": false
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -384,9 +367,7 @@
|
||||
"unrestrictedWebAccess": false,
|
||||
"gambling": false,
|
||||
"kidsAgeBand": null,
|
||||
"ageRatingOverride": "${13|NONE,SEVENTEEN_PLUS,UNRATED|}",
|
||||
"koreaAgeRatingOverride": "${14|NONE,FIFTEEN_PLUS,NINETEEN_PLUS|}",
|
||||
"lootBox": false
|
||||
"seventeenPlus": false
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -461,24 +442,8 @@
|
||||
},
|
||||
"seventeenPlus": {
|
||||
"type": "boolean",
|
||||
"deprecated": true,
|
||||
"deprecationMessage": "This property is deprecated, use `ageRatingOverride` instead.",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"ageRatingOverride": {
|
||||
"$ref": "#/definitions/apple/AppleAgeRatingOverride",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 17, you can manually set the age rating to 17+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"koreaAgeRatingOverride": {
|
||||
"$ref": "#/definitions/apple/AppleKoreaAgeRatingOverride",
|
||||
"description": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n@see https://help.apple.com/app-store-connect/#/dev599d50efb",
|
||||
"markdownDescription": "If your app rates 12+ or lower, and you believe its content may not be suitable for children under 15 or 19, you can manually set the age rating to 15+ or 19+.\n\nIf your app will be distributed through alternative app marketplaces or websites in the European Union and you believe your app is [Unrated](https://developer.apple.com/help/app-store-connect/reference/age-ratings), set this to `UNRATED`.\n\n[Learn more](https://help.apple.com/app-store-connect/#/dev599d50efb)"
|
||||
},
|
||||
"lootBox": {
|
||||
"type": "boolean",
|
||||
"description": "Does your app contain purchasable loot boxes? These virtual containers provide players with in-game items, including functional cards or cosmetic modifications, based on random chance."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -600,20 +565,6 @@
|
||||
"maxLength": 4000
|
||||
}
|
||||
}
|
||||
},
|
||||
"AppleAgeRatingOverride": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"SEVENTEEN_PLUS",
|
||||
"UNRATED"
|
||||
]
|
||||
},
|
||||
"AppleKoreaAgeRatingOverride": {
|
||||
"enum": [
|
||||
"NONE",
|
||||
"FIFTEEN_PLUS",
|
||||
"NINETEEN_PLUS"
|
||||
]
|
||||
}
|
||||
},
|
||||
"AppleConfig": {
|
||||
@@ -824,5 +775,19 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"configVersion": {
|
||||
"enum": [0],
|
||||
"description": "The EAS metadata store configuration version"
|
||||
},
|
||||
"apple": {
|
||||
"$ref": "#/definitions/AppleConfig"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"configVersion",
|
||||
"apple"
|
||||
]
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
function cleanup()
|
||||
{
|
||||
echo 'Cleaning up...'
|
||||
rm -f graphql.schema.json.bak src/graphql/generated.ts.bak
|
||||
}
|
||||
|
||||
# Fail if anything errors
|
||||
set -eox pipefail
|
||||
# If this script exits, trap it first and clean up
|
||||
trap cleanup EXIT
|
||||
|
||||
cp graphql.schema.json graphql.schema.json.bak
|
||||
cp src/graphql/generated.ts src/graphql/generated.ts.bak
|
||||
|
||||
yarn generate-graphql-code
|
||||
if cmp -s graphql.schema.json graphql.schema.json.bak; then
|
||||
echo "GraphQL schema is up-to-date"
|
||||
if cmp -s src/graphql/generated.ts src/graphql/generated.ts.bak; then
|
||||
echo "GraphQL generated code is up-to-date"
|
||||
exit 0
|
||||
else
|
||||
echo "GraphQL code has changed but has not been regenerated. Run `yarn generate-graphql-code` and commit the changes."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "GraphQL schema has changed on the server."
|
||||
echo "Building eas-json"
|
||||
cd ../eas-json
|
||||
yarn build
|
||||
cd ../eas-cli
|
||||
echo "Running `yarn tsc` to check for breaking changes..."
|
||||
yarn tsc
|
||||
# If there are no errors, then the schema is backwards compatible
|
||||
exit 0
|
||||
fi
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -6,7 +6,6 @@ export class ApiV2Error extends Error {
|
||||
readonly expoApiV2ErrorDetails?: JSONValue;
|
||||
readonly expoApiV2ErrorServerStack?: string;
|
||||
readonly expoApiV2ErrorMetadata?: object;
|
||||
readonly expoApiV2ErrorRequestId?: string;
|
||||
|
||||
constructor(response: {
|
||||
message: string;
|
||||
@@ -14,13 +13,11 @@ export class ApiV2Error extends Error {
|
||||
stack?: string;
|
||||
details?: JSONValue;
|
||||
metadata?: object;
|
||||
requestId?: string;
|
||||
}) {
|
||||
super(response.message);
|
||||
this.expoApiV2ErrorCode = response.code;
|
||||
this.expoApiV2ErrorDetails = response.details;
|
||||
this.expoApiV2ErrorServerStack = response.stack;
|
||||
this.expoApiV2ErrorMetadata = response.metadata;
|
||||
this.expoApiV2ErrorRequestId = response.requestId;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,6 @@ describe(ApiV2Client, () => {
|
||||
stack: 'line 1: hello',
|
||||
details: { who: 'world' },
|
||||
metadata: { an: 'object' },
|
||||
requestId: 'request-id',
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -38,7 +37,6 @@ describe(ApiV2Client, () => {
|
||||
expect(error.expoApiV2ErrorDetails).toEqual({ who: 'world' });
|
||||
expect(error.expoApiV2ErrorMetadata).toEqual({ an: 'object' });
|
||||
expect(error.expoApiV2ErrorServerStack).toEqual('line 1: hello');
|
||||
expect(error.expoApiV2ErrorRequestId).toEqual('request-id');
|
||||
});
|
||||
|
||||
it('does not convert non-APIv2 error to ApiV2Error', async () => {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BuildVersionGetView reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL 1`] = `"Aborting..."`;
|
||||
|
||||
exports[`BuildVersionGetView reading version when appVersionSource is set to local 1`] = `"This project is not configured for using remote version source. Add [1m{"cli": { "appVersionSource": "remote" }}[22m 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 {
|
||||
getMockEasJson,
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
withLocalVersionSource,
|
||||
withRemoteVersionSource,
|
||||
} from './utils';
|
||||
import { getMockEasJson, mockCommandContext, mockProjectId, mockTestCommand } from './utils';
|
||||
import BuildVersionGetView from '../../commands/build/version/get';
|
||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||
import Log from '../../log';
|
||||
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
|
||||
import * as prompts from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
@@ -20,7 +12,16 @@ jest.mock('fs');
|
||||
jest.mock('../../log');
|
||||
jest.mock('../../utils/json');
|
||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../prompts');
|
||||
|
||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
||||
return {
|
||||
...easJson,
|
||||
cli: {
|
||||
...easJson.cli,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe(BuildVersionGetView, () => {
|
||||
afterEach(() => {
|
||||
@@ -48,29 +49,6 @@ describe(BuildVersionGetView, () => {
|
||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('reading version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '100',
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
|
||||
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
'ANDROID',
|
||||
'eas.test.com'
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(`Android versionCode - ${chalk.bold('100')}`);
|
||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('reading version for platform android when no remote version is set', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
||||
@@ -167,51 +145,6 @@ describe(BuildVersionGetView, () => {
|
||||
});
|
||||
|
||||
test('reading version when appVersionSource is set to local ', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {
|
||||
easJson: withLocalVersionSource(getMockEasJson()),
|
||||
});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '100',
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
|
||||
const cmd = mockTestCommand(
|
||||
BuildVersionGetView,
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('reading version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '100',
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test('reading version aborts when the appVersionSource is not specified and the user chooses to configure it manually', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '100',
|
||||
storeVersion: '1.0.0',
|
||||
}));
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionGetView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError('Aborted.');
|
||||
});
|
||||
|
||||
test('reading version sets appVersionSource to LOCAL and aborts when the appVersionSource is not specified and is run in non-interactive mode', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionGetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '100',
|
||||
@@ -223,10 +156,6 @@ describe(BuildVersionGetView, () => {
|
||||
['--non-interactive', '--json', '--platform=android'],
|
||||
ctx
|
||||
);
|
||||
await expect(cmd.run()).rejects.toThrowError(
|
||||
`This project is not configured for using remote version source. Add ${chalk.bold(
|
||||
'{"cli": { "appVersionSource": "remote" }}'
|
||||
)} in eas.json or re-run this command without "--non-interactive" flag.`
|
||||
);
|
||||
await expect(cmd.run()).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
@@ -7,15 +8,12 @@ import {
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
withLocalVersionSource,
|
||||
withRemoteVersionSource,
|
||||
} from './utils';
|
||||
import BuildVersionSetView from '../../commands/build/version/set';
|
||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||
import Log from '../../log';
|
||||
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
|
||||
import * as prompts from '../../prompts';
|
||||
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
@@ -27,6 +25,16 @@ jest.mock('../../log');
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../../utils/json');
|
||||
|
||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
||||
return {
|
||||
...easJson,
|
||||
cli: {
|
||||
...easJson.cli,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe(BuildVersionSetView, () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -59,34 +67,6 @@ describe(BuildVersionSetView, () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('setting version for platform android when the appVersionSource is not specified and the user chooses to set it to REMOTE', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
|
||||
version: '1000',
|
||||
}));
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
'ANDROID',
|
||||
'eas.test.com'
|
||||
);
|
||||
expect(AppVersionMutation.createAppVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
expect.objectContaining({
|
||||
buildVersion: '1000',
|
||||
storeVersion: '1.0.0',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('printing current remote version before prompting for a new one', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
||||
@@ -135,9 +115,7 @@ describe(BuildVersionSetView, () => {
|
||||
});
|
||||
|
||||
test('setting version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
||||
easJson: withLocalVersionSource(getMockEasJson()),
|
||||
});
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
|
||||
@@ -147,24 +125,8 @@ describe(BuildVersionSetView, () => {
|
||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('setting version aborts when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
|
||||
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => false);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError('Aborting...');
|
||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('setting version when appVersionSource is set to local and user allows auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {
|
||||
easJson: withLocalVersionSource(getMockEasJson()),
|
||||
});
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
|
||||
@@ -175,33 +137,4 @@ describe(BuildVersionSetView, () => {
|
||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
||||
});
|
||||
|
||||
test('setting version when the appVersionSource is not specified and the user chooses to set it to LOCAL, and they allow auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
|
||||
jest.mocked(prompts.confirmAsync).mockImplementationOnce(async () => true);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
||||
await cmd.run();
|
||||
|
||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
||||
});
|
||||
|
||||
test('setting version aborts when the appVersionSource is not specified and the user chooses to configure manually', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSetView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSetView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError('Aborted.');
|
||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
@@ -8,8 +9,6 @@ import {
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
withLocalVersionSource,
|
||||
withRemoteVersionSource,
|
||||
} from './utils';
|
||||
import { updateNativeVersionsAsync as updateAndroidNativeVersionsAsync } from '../../build/android/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 { getAppBuildGradleAsync } from '../../project/android/gradleUtils';
|
||||
import { resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { AppVersionSourceUpdateOption } from '../../project/remoteVersionSource';
|
||||
import { resolveWorkflowAsync } from '../../project/workflow';
|
||||
import * as prompts from '../../prompts';
|
||||
|
||||
@@ -39,6 +37,16 @@ jest.mock('../../log');
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../../utils/json');
|
||||
|
||||
function withRemoteVersionSource(easJson: EasJson): EasJson {
|
||||
return {
|
||||
...easJson,
|
||||
cli: {
|
||||
...easJson.cli,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe(BuildVersionSyncView, () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -71,34 +79,6 @@ describe(BuildVersionSyncView, () => {
|
||||
expect(syncAndroidAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('syncing version for managed project on platform android when appVersionSource is not set and the user chooses to set it to REMOTE', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => ({
|
||||
buildVersion: '1000',
|
||||
storeVersion: '0.0.1',
|
||||
}));
|
||||
jest.mocked(prompts.promptAsync).mockImplementationOnce(async () => ({
|
||||
version: '1000',
|
||||
}));
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_REMOTE);
|
||||
jest.mocked(resolveWorkflowAsync).mockImplementation(async () => Workflow.MANAGED);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
||||
const syncAndroidAsync = jest.spyOn(cmd, 'syncAndroidAsync' as any);
|
||||
|
||||
await cmd.run();
|
||||
expect(AppVersionQuery.latestVersionAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
'ANDROID',
|
||||
'eas.test.com'
|
||||
);
|
||||
expect(syncAndroidAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('syncing version for bare project on platform android', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
||||
easJson: withRemoteVersionSource(getMockEasJson()),
|
||||
@@ -199,25 +179,9 @@ describe(BuildVersionSyncView, () => {
|
||||
});
|
||||
|
||||
test('syncing version aborts when appVersionSource is set to local and users refuse auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
||||
easJson: withLocalVersionSource(getMockEasJson()),
|
||||
});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError('Aborting...');
|
||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
test('syncing version aborts when appVersionSource is not set and the user chooses to set it to LOCAL, and they refuse auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => false);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
||||
@@ -226,9 +190,7 @@ describe(BuildVersionSyncView, () => {
|
||||
});
|
||||
|
||||
test('syncing version when appVersionSource is set to local and user allows auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {
|
||||
easJson: withLocalVersionSource(getMockEasJson()),
|
||||
});
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
|
||||
@@ -239,33 +201,4 @@ describe(BuildVersionSyncView, () => {
|
||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
||||
});
|
||||
|
||||
test('syncing version when appVersionSource is not set and the user chooses to set it to LOCAL and they allow auto configuration', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.SET_TO_LOCAL);
|
||||
jest.mocked(prompts.confirmAsync).mockImplementation(async () => true);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
||||
await cmd.run();
|
||||
|
||||
const easJsonAfterCmd = await fs.readJson(path.join(ctx.projectDir, 'eas.json'));
|
||||
expect(easJsonAfterCmd.cli.appVersionSource).toBe('remote');
|
||||
});
|
||||
|
||||
test('syncing version aborts when appVersionSource is not set and the user chooses to configure manually', async () => {
|
||||
const ctx = mockCommandContext(BuildVersionSyncView, {});
|
||||
jest.mocked(AppVersionQuery.latestVersionAsync).mockImplementation(async () => null);
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
jest
|
||||
.mocked(prompts.selectAsync)
|
||||
.mockImplementationOnce(async () => AppVersionSourceUpdateOption.ABORT);
|
||||
|
||||
const cmd = mockTestCommand(BuildVersionSyncView, ['--platform=android'], ctx);
|
||||
await expect(cmd.run()).rejects.toThrowError('Aborted.');
|
||||
expect(AppVersionMutation.createAppVersionAsync).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { EasJson } from '@expo/eas-json';
|
||||
import { Command, Config } from '@oclif/core';
|
||||
import { vol } from 'memfs';
|
||||
import path from 'path';
|
||||
@@ -7,16 +7,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { ContextInput, ContextOutput } from '../../commandUtils/EasCommand';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import {
|
||||
AppFragment,
|
||||
Role,
|
||||
WorkflowJobStatus,
|
||||
WorkflowJobType,
|
||||
WorkflowRunByIdWithJobsQuery,
|
||||
WorkflowRunFragment,
|
||||
WorkflowRunStatus,
|
||||
WorkflowRunTriggerEventType,
|
||||
} from '../../graphql/generated';
|
||||
import { AppFragment, Role } from '../../graphql/generated';
|
||||
|
||||
export function getMockEasJson(): EasJson {
|
||||
return {
|
||||
@@ -33,7 +24,6 @@ export function getMockAppFragment(): AppFragment {
|
||||
return {
|
||||
id: mockProjectId,
|
||||
slug: 'testapp',
|
||||
name: 'testapp',
|
||||
fullName: '@testuser/testpp',
|
||||
ownerAccount: {
|
||||
id: 'test-account-id',
|
||||
@@ -90,9 +80,7 @@ export function mockCommandContext<
|
||||
if (contextKey === 'loggedIn') {
|
||||
result.loggedIn = {};
|
||||
}
|
||||
if (contextKey === 'projectId') {
|
||||
result.projectId = overrides.projectId ?? mockProjectId;
|
||||
}
|
||||
|
||||
if (contextKey === 'projectDir') {
|
||||
result.projectDir = projectDir;
|
||||
}
|
||||
@@ -159,204 +147,3 @@ export const getError = <TError = any>(call: () => unknown): TError | NoErrorThr
|
||||
return error as TError;
|
||||
}
|
||||
};
|
||||
|
||||
export function withRemoteVersionSource(easJson: EasJson): EasJson {
|
||||
return {
|
||||
...easJson,
|
||||
cli: {
|
||||
...easJson.cli,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function withLocalVersionSource(easJson: EasJson): EasJson {
|
||||
return {
|
||||
...easJson,
|
||||
cli: {
|
||||
...easJson.cli,
|
||||
appVersionSource: AppVersionSource.LOCAL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockEmptyWorkflowRunsFragment(): WorkflowRunFragment[] {
|
||||
return getMockWorkflowRunsFragment();
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunsFragment(
|
||||
params?:
|
||||
| undefined
|
||||
| {
|
||||
successes?: number;
|
||||
failures?: number;
|
||||
pending?: number;
|
||||
withJobs?: number;
|
||||
withBuildJobs?: number;
|
||||
}
|
||||
): WorkflowRunFragment[] {
|
||||
const {
|
||||
successes = 0,
|
||||
failures = 0,
|
||||
pending = 0,
|
||||
withJobs = 0,
|
||||
withBuildJobs = 0,
|
||||
} = params ?? {};
|
||||
const runs: any[] = [];
|
||||
for (let i = 0; i < successes; i++) {
|
||||
runs.push({
|
||||
id: `success-${i}`,
|
||||
status: WorkflowRunStatus.Success,
|
||||
errors: [],
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
workflow: {
|
||||
name: 'build',
|
||||
id: 'build',
|
||||
fileName: 'build.yml',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < failures; i++) {
|
||||
runs.push({
|
||||
id: `failure-${i}`,
|
||||
errors: ['Failed'],
|
||||
status: WorkflowRunStatus.Failure,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
workflow: {
|
||||
name: 'build',
|
||||
id: 'build',
|
||||
fileName: 'build.yml',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < pending; i++) {
|
||||
runs.push({
|
||||
id: `pending-${i}`,
|
||||
errors: [],
|
||||
status: WorkflowRunStatus.InProgress,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
workflow: {
|
||||
name: 'build',
|
||||
id: 'build',
|
||||
fileName: 'build.yml',
|
||||
},
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < withJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithJobsFragment());
|
||||
}
|
||||
for (let i = 0; i < withBuildJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithBuildJobsFragment());
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
export function getMockWorkflowCustomJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
id: 'job1',
|
||||
key: 'job1',
|
||||
name: 'job1',
|
||||
status: WorkflowJobStatus.Success,
|
||||
type: WorkflowJobType.Custom,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
outputs: {},
|
||||
errors: [],
|
||||
workflowRun: {
|
||||
id: runId ?? 'build1',
|
||||
},
|
||||
turtleJobRun: {
|
||||
id: 'job1',
|
||||
logFileUrls: ['https://example.com/log1'],
|
||||
errors: [],
|
||||
artifacts: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowBuildJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
...getMockWorkflowCustomJobFragment(runId),
|
||||
type: WorkflowJobType.Build,
|
||||
outputs: {
|
||||
build_id: 'build1',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunWithJobsFragment(
|
||||
runID?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
|
||||
const id = runID ?? 'build1';
|
||||
return {
|
||||
id,
|
||||
status: WorkflowRunStatus.Failure,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
errors: [],
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
app: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowCustomJobFragment(id)],
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunWithBuildJobsFragment(
|
||||
runID?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
|
||||
const id = runID ?? 'build1';
|
||||
return {
|
||||
id,
|
||||
status: WorkflowRunStatus.Failure,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
errors: [],
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
app: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowBuildJobFragment(id)],
|
||||
};
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
import {
|
||||
getMockEmptyWorkflowRunsFragment,
|
||||
getMockWorkflowCustomJobFragment,
|
||||
getMockWorkflowRunWithJobsFragment,
|
||||
getMockWorkflowRunsFragment,
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
} from './utils';
|
||||
import {
|
||||
fetchRawLogsForBuildJobAsync,
|
||||
fetchRawLogsForCustomJobAsync,
|
||||
} from '../../commandUtils/workflow/fetchLogs';
|
||||
import WorkflowLogView from '../../commands/workflow/logs';
|
||||
import { AppPlatform, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
jest.mock('../../build/android/version');
|
||||
jest.mock('../../build/ios/version');
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../graphql/queries/AppQuery');
|
||||
jest.mock('../../graphql/queries/BuildQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowJobQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowRunQuery');
|
||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../project/workflow');
|
||||
jest.mock('../../project/android/gradleUtils');
|
||||
jest.mock('../../project/ios/target');
|
||||
jest.mock('../../project/ios/scheme');
|
||||
jest.mock('fs');
|
||||
jest.mock('../../log');
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../../utils/json');
|
||||
jest.mock('../../commandUtils/workflow/fetchLogs');
|
||||
|
||||
describe(WorkflowLogView, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowCustomJobFragment());
|
||||
jest
|
||||
.mocked(WorkflowRunQuery.withJobsByIdAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('view logs, passing in no parameters, no runs found', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(Log.error).toHaveBeenCalledWith('No workflow runs found');
|
||||
});
|
||||
test('view logs, passing in no parameters, runs found, no jobs', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ successes: 1 }));
|
||||
jest.mocked(promptAsync).mockResolvedValue({ selectedRun: 'build1' });
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(Log.error).toHaveBeenCalledWith('No job found');
|
||||
});
|
||||
test('view logs, passing in no parameters, runs found, jobs found, step selected, no logs', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
|
||||
let promptCalls = 0;
|
||||
jest.mocked(promptAsync).mockImplementation(async () => {
|
||||
if (promptCalls === 0) {
|
||||
promptCalls++;
|
||||
return { selectedRun: 'build1' };
|
||||
} else if (promptCalls === 1) {
|
||||
promptCalls++;
|
||||
return { selectedJob: 0 };
|
||||
} else {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest.mocked(fetchRawLogsForCustomJobAsync).mockResolvedValue(null);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(promptAsync).toHaveBeenCalledTimes(2);
|
||||
expect(Log.error).toHaveBeenCalledWith('No logs found');
|
||||
});
|
||||
test('view logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ withJobs: 1 }));
|
||||
let promptCalls = 0;
|
||||
jest.mocked(promptAsync).mockImplementation(async () => {
|
||||
if (promptCalls === 0) {
|
||||
promptCalls++;
|
||||
return { selectedRun: 'build1' };
|
||||
} else if (promptCalls === 1) {
|
||||
promptCalls++;
|
||||
return { selectedJob: 0 };
|
||||
} else {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForCustomJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(promptAsync).toHaveBeenCalledTimes(3);
|
||||
expect(Log.error).not.toHaveBeenCalled();
|
||||
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
|
||||
});
|
||||
test('view build logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ withBuildJobs: 1 }));
|
||||
jest.mocked(BuildQuery.byIdAsync).mockResolvedValue({
|
||||
id: 'build1',
|
||||
status: BuildStatus.Finished,
|
||||
priority: BuildPriority.Normal,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
isForIosSimulator: false,
|
||||
project: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
slug: 'app',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
platform: AppPlatform.Android,
|
||||
logFiles: ['https://example.com/log1'],
|
||||
});
|
||||
let promptCalls = 0;
|
||||
jest.mocked(promptAsync).mockImplementation(async () => {
|
||||
if (promptCalls === 0) {
|
||||
promptCalls++;
|
||||
return { selectedRun: 'build1' };
|
||||
} else if (promptCalls === 1) {
|
||||
promptCalls++;
|
||||
return { selectedJob: 0 };
|
||||
} else {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForBuildJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(promptAsync).toHaveBeenCalledTimes(3);
|
||||
expect(Log.error).not.toHaveBeenCalled();
|
||||
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
|
||||
});
|
||||
test('view logs for a workflow job, passing in a job ID, all steps', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, ['job1', '--all-steps'], ctx);
|
||||
await cmd.run();
|
||||
expect(WorkflowJobQuery.byIdAsync).toHaveBeenCalledWith(ctx.loggedIn.graphqlClient, 'job1', {
|
||||
useCache: false,
|
||||
});
|
||||
});
|
||||
test('view logs for a workflow job, passing in a run ID', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
|
||||
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--all-steps'], ctx);
|
||||
await cmd.run();
|
||||
expect(WorkflowRunQuery.withJobsByIdAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
'build1',
|
||||
{
|
||||
useCache: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
test('view logs for a workflow job, passing in a run ID, non-interactive', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest.mocked(promptAsync).mockResolvedValue({ selectedJob: 'job1' });
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockRejectedValue(new Error('Job not found'));
|
||||
const cmd = mockTestCommand(WorkflowLogView, ['build1', '--non-interactive'], ctx);
|
||||
try {
|
||||
await cmd.run();
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(Error);
|
||||
expect((e as Error).message).toEqual('No workflow job found that matched the provided ID');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,184 +0,0 @@
|
||||
import {
|
||||
getMockEmptyWorkflowRunsFragment,
|
||||
getMockWorkflowRunsFragment,
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
} from './utils';
|
||||
import WorkflowRunList from '../../commands/workflow/runs';
|
||||
import { WorkflowRunStatus, WorkflowRunTriggerEventType } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
jest.mock('../../build/android/version');
|
||||
jest.mock('../../build/ios/version');
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../graphql/queries/AppQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowRunQuery');
|
||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../project/workflow');
|
||||
jest.mock('../../project/android/gradleUtils');
|
||||
jest.mock('../../project/ios/target');
|
||||
jest.mock('../../project/ios/scheme');
|
||||
jest.mock('fs');
|
||||
jest.mock('../../log');
|
||||
jest.mock('../../prompts');
|
||||
jest.mock('../../utils/json');
|
||||
|
||||
describe(WorkflowRunList, () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('list workflow runs with default params', async () => {
|
||||
const ctx = mockCommandContext(WorkflowRunList, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
|
||||
const cmd = mockTestCommand(WorkflowRunList, [], ctx);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
10
|
||||
);
|
||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
test('list workflow runs with custom limit', async () => {
|
||||
const ctx = mockCommandContext(WorkflowRunList, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockEmptyWorkflowRunsFragment());
|
||||
const cmd = mockTestCommand(WorkflowRunList, ['--limit', '100'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
100
|
||||
);
|
||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
test('list workflow runs with specific filename', async () => {
|
||||
const ctx = mockCommandContext(WorkflowRunList, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
|
||||
{
|
||||
id: 'build',
|
||||
status: WorkflowRunStatus.Success,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
errors: [],
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const cmd = mockTestCommand(WorkflowRunList, ['--workflow', 'build.yml'], ctx);
|
||||
await cmd.run();
|
||||
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
'build.yml',
|
||||
undefined,
|
||||
10
|
||||
);
|
||||
expect(enableJsonOutput).not.toHaveBeenCalled();
|
||||
expect(printJsonOnlyOutput).not.toHaveBeenCalled();
|
||||
});
|
||||
test('list only workflow runs with status FAILURE, get json output', async () => {
|
||||
const ctx = mockCommandContext(WorkflowRunList, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ failures: 1 }));
|
||||
const cmd = mockTestCommand(WorkflowRunList, ['--status', 'FAILURE', '--json'], ctx);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
WorkflowRunStatus.Failure,
|
||||
10
|
||||
);
|
||||
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'failure-0',
|
||||
status: 'FAILURE',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
trigger: '',
|
||||
triggerType: 'Other',
|
||||
startedAt: '2022-01-01T00:00:00.000Z',
|
||||
finishedAt: '2022-01-01T00:00:00.000Z',
|
||||
workflowId: 'build',
|
||||
workflowName: 'build',
|
||||
workflowFileName: 'build.yml',
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('list only workflow runs with specific filename and status FAILURE, get json output', async () => {
|
||||
const ctx = mockCommandContext(WorkflowRunList, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
jest.mocked(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).mockResolvedValue([
|
||||
{
|
||||
id: 'build1',
|
||||
status: WorkflowRunStatus.Failure,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
errors: [],
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
},
|
||||
},
|
||||
]);
|
||||
const cmd = mockTestCommand(
|
||||
WorkflowRunList,
|
||||
['--workflow', 'build.yml', '--status', 'FAILURE', '--json'],
|
||||
ctx
|
||||
);
|
||||
await cmd.run();
|
||||
expect(WorkflowRunQuery.byAppIdFileNameAndStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
'build.yml',
|
||||
WorkflowRunStatus.Failure,
|
||||
10
|
||||
);
|
||||
expect(printJsonOnlyOutput).toHaveBeenCalledWith([
|
||||
{
|
||||
id: 'build1',
|
||||
status: 'FAILURE',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
trigger: '',
|
||||
triggerType: 'Manual',
|
||||
startedAt: '2022-01-01T00:00:00.000Z',
|
||||
finishedAt: '2022-01-01T00:00:00.000Z',
|
||||
workflowId: 'build',
|
||||
workflowName: 'build',
|
||||
workflowFileName: 'build.yml',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -95,11 +95,7 @@ export function getExpoWebsiteBaseUrl(): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string, manifestHostOverride: string | null): string {
|
||||
if (manifestHostOverride) {
|
||||
return new URL(projectId, `https://${manifestHostOverride}`).href;
|
||||
}
|
||||
|
||||
export function getEASUpdateURL(projectId: string): string {
|
||||
if (process.env.EXPO_STAGING) {
|
||||
return new URL(projectId, `https://staging-u.expo.dev`).href;
|
||||
} else if (process.env.EXPO_LOCAL) {
|
||||
@@ -108,7 +104,3 @@ export function getEASUpdateURL(projectId: string, manifestHostOverride: string
|
||||
return new URL(projectId, `https://u.expo.dev`).href;
|
||||
}
|
||||
}
|
||||
|
||||
export function getExpoApiWorkflowSchemaURL(): string {
|
||||
return getExpoApiBaseUrl() + '/v2/workflows/schema';
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
*/
|
||||
export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> {
|
||||
constructor(
|
||||
private readonly options: {
|
||||
private options: {
|
||||
filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean;
|
||||
printedType?: string;
|
||||
pageSize?: number;
|
||||
@@ -91,8 +91,8 @@ export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragme
|
||||
queryParams: QueryParams
|
||||
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
|
||||
filterPredicate
|
||||
? await this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
||||
: await this.queryAsync(ctx, queryParams);
|
||||
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
||||
: this.queryAsync(ctx, queryParams);
|
||||
|
||||
const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> =>
|
||||
branchInfo.name;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
BackgroundJobReceiptDataFragment,
|
||||
ScheduleBranchDeletionMutation,
|
||||
ScheduleBranchDeletionMutationVariables,
|
||||
} from '../graphql/generated';
|
||||
import { BackgroundJobReceiptNode } from '../graphql/types/BackgroundJobReceipt';
|
||||
|
||||
export async function scheduleBranchDeletionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
branchId,
|
||||
}: {
|
||||
branchId: string;
|
||||
}
|
||||
): Promise<BackgroundJobReceiptDataFragment> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<ScheduleBranchDeletionMutation, ScheduleBranchDeletionMutationVariables>(
|
||||
gql`
|
||||
mutation ScheduleBranchDeletion($branchId: ID!) {
|
||||
updateBranch {
|
||||
scheduleUpdateBranchDeletion(branchId: $branchId) {
|
||||
id
|
||||
...BackgroundJobReceiptData
|
||||
}
|
||||
}
|
||||
}
|
||||
${BackgroundJobReceiptNode}
|
||||
`,
|
||||
{ branchId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return result.updateBranch.scheduleUpdateBranchDeletion;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { BranchNotFoundError } from './utils';
|
||||
@@ -9,11 +8,10 @@ import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
CreateUpdateBranchForAppMutation,
|
||||
CreateUpdateBranchForAppMutationVariables,
|
||||
UpdateBranchBasicInfoFragment,
|
||||
UpdateBranch,
|
||||
UpdateBranchFragment,
|
||||
} from '../graphql/generated';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import { UpdateBranchBasicInfoFragmentNode } from '../graphql/types/UpdateBranchBasicInfo';
|
||||
import Log from '../log';
|
||||
import { formatBranch, getBranchDescription } from '../update/utils';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
@@ -86,9 +84,7 @@ export async function listAndRenderBranchesOnAppAsync(
|
||||
queryBranchesOnProjectAsync(graphqlClient, limit, offset, projectId),
|
||||
promptOptions: {
|
||||
title: 'Load more branches?',
|
||||
renderListItems: branches => {
|
||||
renderPageOfBranches(branches, paginatedQueryOptions);
|
||||
},
|
||||
renderListItems: branches => renderPageOfBranches(branches, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -128,7 +124,7 @@ function renderPageOfBranches(
|
||||
export async function createUpdateBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ appId, name }: CreateUpdateBranchForAppMutationVariables
|
||||
): Promise<UpdateBranchBasicInfoFragment> {
|
||||
): Promise<Pick<UpdateBranch, 'id' | 'name'>> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<CreateUpdateBranchForAppMutation, CreateUpdateBranchForAppMutationVariables>(
|
||||
@@ -137,17 +133,15 @@ export async function createUpdateBranchOnAppAsync(
|
||||
updateBranch {
|
||||
createUpdateBranchForApp(appId: $appId, name: $name) {
|
||||
id
|
||||
...UpdateBranchBasicInfoFragment
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
${print(UpdateBranchBasicInfoFragmentNode)}
|
||||
`,
|
||||
{
|
||||
appId,
|
||||
name,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch'] }
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
@@ -167,21 +161,22 @@ export async function ensureBranchExistsAsync(
|
||||
appId: string;
|
||||
branchName: string;
|
||||
}
|
||||
): Promise<{ branch: UpdateBranchBasicInfoFragment; createdBranch: boolean }> {
|
||||
): Promise<{ branchId: string; createdBranch: boolean }> {
|
||||
try {
|
||||
const updateBranch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
|
||||
return { branch: updateBranch, createdBranch: false };
|
||||
const { id } = updateBranch;
|
||||
return { branchId: id, createdBranch: false };
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newUpdateBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
appId,
|
||||
name: branchName,
|
||||
});
|
||||
return { branch: newUpdateBranch, createdBranch: true };
|
||||
return { branchId: newUpdateBranch.id, createdBranch: true };
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string | null> {
|
||||
return await vcsClient.getBranchNameAsync();
|
||||
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
|
||||
return (
|
||||
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
|
||||
);
|
||||
}
|
||||
|
||||
export class BranchNotFoundError extends Error {
|
||||
|
||||
@@ -47,14 +47,14 @@ describe(selectBuildToCancelAsync.name, () => {
|
||||
it('does not return build id when confirmation is rejected', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||
await expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
||||
});
|
||||
|
||||
it('returns build id when confirmation is confirmed', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
await expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
||||
expect(selectBuildToCancelAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
||||
selectedBuildId
|
||||
);
|
||||
});
|
||||
@@ -72,7 +72,6 @@ function createMockBuildFragment({
|
||||
updatedAt: new Date().toISOString(),
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
logFiles: [],
|
||||
priority: BuildPriority.Normal,
|
||||
project: {
|
||||
__typename: 'App',
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -47,14 +47,14 @@ describe(selectBuildToDeleteAsync.name, () => {
|
||||
it('does not return build id when confirmation is rejected', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(false);
|
||||
await expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
||||
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(null);
|
||||
});
|
||||
|
||||
it('returns build id when confirmation is confirmed', async () => {
|
||||
const graphqlClient = instance(mock<ExpoGraphqlClient>());
|
||||
jest.mocked(selectAsync).mockResolvedValueOnce(selectedBuildId);
|
||||
jest.mocked(confirmAsync).mockResolvedValueOnce(true);
|
||||
await expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
||||
expect(selectBuildToDeleteAsync(graphqlClient, projectId, 'blah')).resolves.toEqual(
|
||||
selectedBuildId
|
||||
);
|
||||
});
|
||||
@@ -70,7 +70,6 @@ function createMockBuildFragment({
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
logFiles: [],
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
priority: BuildPriority.Normal,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import Log from '../../../log';
|
||||
import { maybeWarnAboutNonStandardBuildType } from '../build';
|
||||
|
||||
const warn = jest.spyOn(Log, 'warn');
|
||||
|
||||
describe('maybeWarnAboutNonStandardBuildType', () => {
|
||||
it('should not warn about non-standard build type', () => {
|
||||
const buildProfile = {
|
||||
gradleCommand: ':app:buildRelease',
|
||||
};
|
||||
|
||||
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'release' });
|
||||
|
||||
expect(warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should warn about non-standard build type', () => {
|
||||
const buildProfile = {
|
||||
gradleCommand: ':app:buildStage',
|
||||
};
|
||||
|
||||
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: 'stage' });
|
||||
|
||||
expect(warn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,43 +1,23 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
import { AndroidBuildProfile } from '@expo/eas-json/build/build/types';
|
||||
import assert from 'assert';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
|
||||
import { resolveVcsClient } from '../../../vcs';
|
||||
import { Client } from '../../../vcs/vcs';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
bumpVersionInAppJsonAsync,
|
||||
maybeResolveVersionsAsync,
|
||||
resolveRemoteVersionCodeAsync,
|
||||
} from '../version';
|
||||
|
||||
const fsReal = jest.requireActual('fs').promises as typeof fs;
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
|
||||
jest.mock('../../../vcs/vcs');
|
||||
jest.mock('../../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
succeed: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
@@ -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({
|
||||
versionCode = 123,
|
||||
versionName = '3.0.0',
|
||||
}: {
|
||||
versionCode?: number | null;
|
||||
versionName?: string | null;
|
||||
versionCode?: number;
|
||||
versionName?: string;
|
||||
} = {}): ExpoConfig {
|
||||
const fakeExp: ExpoConfig = {
|
||||
name: 'myproject',
|
||||
@@ -407,8 +239,8 @@ function initBareWorkflowProject({
|
||||
'./android/app/build.gradle': `android {
|
||||
defaultConfig {
|
||||
applicationId "com.expo.testapp"
|
||||
${versionCode ? `versionCode "${versionCode}"` : ''}
|
||||
${versionName ? `versionName "${versionName}"` : ''}
|
||||
versionCode ${versionCode}
|
||||
versionName "${versionName}"
|
||||
}
|
||||
}`,
|
||||
'./android/app/src/main/AndroidManifest.xml': 'fake',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Android, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Android, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import getenv from 'getenv';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { transformJob } from './graphql';
|
||||
@@ -11,9 +10,9 @@ import { resolveRemoteVersionCodeAsync } from './version';
|
||||
import AndroidCredentialsProvider, {
|
||||
AndroidCredentials,
|
||||
} from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import Log from '../../log';
|
||||
import {
|
||||
ensureApplicationIdIsDefinedForManagedProjectAsync,
|
||||
getApplicationIdAsync,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
prepareBuildRequestForPlatformAsync,
|
||||
} from '../build';
|
||||
import { AndroidBuildContext, BuildContext, CommonContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import { logCredentialsSource } from '../utils/credentials';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
@@ -65,10 +65,6 @@ This means that it will most likely produce an AAB and you will not be able to i
|
||||
ctx.vcsClient
|
||||
);
|
||||
|
||||
if (gradleContext?.buildType) {
|
||||
maybeWarnAboutNonStandardBuildType({ buildProfile, buildType: gradleContext.buildType });
|
||||
}
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
|
||||
}
|
||||
@@ -111,7 +107,6 @@ export async function prepareAndroidBuildAsync(
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
vcsClient: ctx.vcsClient,
|
||||
env: ctx.env,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
@@ -123,9 +118,10 @@ export async function prepareAndroidBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Android.Job,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createAndroidBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
@@ -154,7 +150,6 @@ async function ensureAndroidCredentialsAsync(
|
||||
ctx.android.gradleContext
|
||||
);
|
||||
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
|
||||
name: ctx.buildProfile.keystoreName,
|
||||
app: {
|
||||
account: nullthrows(
|
||||
ctx.user.accounts.find(a => a.name === ctx.accountName),
|
||||
@@ -171,38 +166,3 @@ async function ensureAndroidCredentialsAsync(
|
||||
source: credentialsSource,
|
||||
};
|
||||
}
|
||||
|
||||
export function maybeWarnAboutNonStandardBuildType({
|
||||
buildProfile,
|
||||
buildType,
|
||||
}: {
|
||||
buildProfile: Pick<CommonContext<Platform.ANDROID>['buildProfile'], 'gradleCommand'>;
|
||||
buildType: string;
|
||||
}): void {
|
||||
const shouldSuppressWarning = getenv.boolish('EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING', false);
|
||||
|
||||
if (shouldSuppressWarning) {
|
||||
return;
|
||||
}
|
||||
|
||||
const knownValidBuildTypes = ['debug', 'release'];
|
||||
if (!knownValidBuildTypes.includes(buildType)) {
|
||||
Log.warn(
|
||||
`Custom gradle command "${chalk.bold(
|
||||
buildProfile.gradleCommand
|
||||
)}" has non-standard build type: "${chalk.bold(
|
||||
buildType
|
||||
)}". Expected the command to end with "${chalk.bold('...Debug')}" or "${chalk.bold(
|
||||
'...Release'
|
||||
)}"`
|
||||
);
|
||||
Log.warn(
|
||||
`Ensure the command is spelled correctly and ${chalk.bold(
|
||||
'build.gradle'
|
||||
)} is configured correctly. To suppress this warning, set ${chalk.bold(
|
||||
'EAS_BUILD_NO_GRADLE_BUILD_TYPE_WARNING=true'
|
||||
)}.`
|
||||
);
|
||||
Log.warn(learnMore('https://developer.android.com/build/build-variants#build-types'));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: job.projectRootDirectory,
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
@@ -29,7 +30,6 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -12,7 +12,7 @@ import slash from 'slash';
|
||||
|
||||
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
|
||||
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
|
||||
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
@@ -29,7 +29,7 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.ANDROID>,
|
||||
jobData: JobData
|
||||
): Promise<Android.Job> {
|
||||
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
@@ -56,13 +56,6 @@ export async function prepareJobAsync(
|
||||
? getCustomBuildConfigPathForJob(buildProfile.config)
|
||||
: undefined;
|
||||
|
||||
let buildMode;
|
||||
if (buildProfile.config) {
|
||||
buildMode = BuildMode.CUSTOM;
|
||||
} else {
|
||||
buildMode = BuildMode.BUILD;
|
||||
}
|
||||
|
||||
const job: Android.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.ANDROID,
|
||||
@@ -71,15 +64,12 @@ export async function prepareJobAsync(
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
corepack: buildProfile.corepack,
|
||||
pnpm: buildProfile.pnpm,
|
||||
bun: buildProfile.bun,
|
||||
yarn: buildProfile.yarn,
|
||||
ndk: buildProfile.ndk,
|
||||
env: {
|
||||
...buildProfile.env,
|
||||
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
|
||||
},
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
@@ -89,12 +79,12 @@ export async function prepareJobAsync(
|
||||
secrets: {
|
||||
...buildCredentials,
|
||||
},
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
gradleCommand: buildProfile.gradleCommand,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
environment: ctx.buildProfile.environment,
|
||||
buildType,
|
||||
username,
|
||||
...(ctx.android.versionCodeOverride && {
|
||||
@@ -105,7 +95,7 @@ export async function prepareJobAsync(
|
||||
experimental: {
|
||||
prebuildCommand: buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: buildMode,
|
||||
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
...(maybeCustomBuildConfigPath && {
|
||||
customBuildConfig: {
|
||||
@@ -113,10 +103,6 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AndroidConfig } from '@expo/config-plugins';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AndroidVersionAutoIncrement } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
@@ -18,13 +18,11 @@ export async function syncProjectConfigurationAsync({
|
||||
exp,
|
||||
localAutoIncrement,
|
||||
vcsClient,
|
||||
env,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
localAutoIncrement?: AndroidVersionAutoIncrement;
|
||||
vcsClient: Client;
|
||||
env: Env | undefined;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
@@ -32,7 +30,7 @@ export async function syncProjectConfigurationAsync({
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
await cleanUpOldEasBuildGradleScriptAsync(projectDir);
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync({ projectDir, exp, workflow, env });
|
||||
await syncUpdatesConfigurationAsync(projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy });
|
||||
} else {
|
||||
|
||||
@@ -44,7 +44,7 @@ export async function bumpVersionAsync({
|
||||
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
const isMultiFlavor =
|
||||
buildGradle.android?.productFlavors ?? buildGradle.android?.flavorDimensions;
|
||||
buildGradle.android?.productFlavors || buildGradle.android?.flavorDimensions;
|
||||
if (isMultiFlavor) {
|
||||
throw new Error(
|
||||
'Automatic version bumping is not supported for multi-flavor Android projects.'
|
||||
@@ -99,7 +99,7 @@ export async function maybeResolveVersionsAsync(
|
||||
exp: ExpoConfig,
|
||||
buildProfile: BuildProfile<Platform.ANDROID>,
|
||||
vcsClient: Client
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string; isVersionInitialized?: boolean }> {
|
||||
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
const buildGradle = await getAppBuildGradleAsync(projectDir);
|
||||
@@ -113,11 +113,6 @@ export async function maybeResolveVersionsAsync(
|
||||
resolveConfigValue(buildGradle, 'versionName', parsedGradleCommand?.flavor) ?? '1.0.0',
|
||||
appBuildVersion:
|
||||
resolveConfigValue(buildGradle, 'versionCode', parsedGradleCommand?.flavor) ?? '1',
|
||||
isVersionInitialized: !resolveConfigValue(
|
||||
buildGradle,
|
||||
'versionCode',
|
||||
parsedGradleCommand?.flavor
|
||||
),
|
||||
};
|
||||
} catch {
|
||||
return {};
|
||||
@@ -209,18 +204,14 @@ export async function resolveRemoteVersionCodeAsync(
|
||||
|
||||
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
|
||||
let currentBuildVersion: string;
|
||||
let shouldInitializeVersionCode = false;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localVersions.appBuildVersion) {
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
|
||||
)
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, versionCode will be initialized based on the value from the local project.'
|
||||
);
|
||||
currentBuildVersion = localVersions.appBuildVersion;
|
||||
shouldInitializeVersionCode = localVersions.isVersionInitialized ?? false;
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
@@ -230,10 +221,7 @@ export async function resolveRemoteVersionCodeAsync(
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (
|
||||
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
|
||||
shouldInitializeVersionCode
|
||||
) {
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
const spinner = ora(
|
||||
`Initializing versionCode with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
|
||||
@@ -34,10 +34,8 @@ import { collectMetadataAsync } from './metadata';
|
||||
import { printDeprecationWarnings } from './utils/printBuildInfo';
|
||||
import {
|
||||
LocalFile,
|
||||
assertProjectTarballSizeDoesNotExceedLimit,
|
||||
makeProjectMetadataFileAsync,
|
||||
makeProjectTarballAsync,
|
||||
maybeWarnAboutProjectTarballSize,
|
||||
reviewAndCommitChangesAsync,
|
||||
} from './utils/repository';
|
||||
import { BuildEvent } from '../analytics/AnalyticsManager';
|
||||
@@ -45,15 +43,12 @@ import { withAnalyticsAsync } from '../analytics/common';
|
||||
import { getExpoWebsiteBaseUrl } from '../api';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasCommandError } from '../commandUtils/errors';
|
||||
import { createFingerprintAsync } from '../fingerprint/cli';
|
||||
import {
|
||||
AppPlatform,
|
||||
BuildFragment,
|
||||
BuildMetadataInput,
|
||||
BuildParamsInput,
|
||||
BuildPriority,
|
||||
BuildStatus,
|
||||
FingerprintSourceInput,
|
||||
UploadSessionType,
|
||||
} from '../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../graphql/mutations/BuildMutation';
|
||||
@@ -61,13 +56,10 @@ import { BuildQuery } from '../graphql/queries/BuildQuery';
|
||||
import Log, { learnMore, link } from '../log';
|
||||
import { Ora, ora } from '../ora';
|
||||
import {
|
||||
RequestedPlatform,
|
||||
appPlatformDisplayNames,
|
||||
appPlatformEmojis,
|
||||
requestedPlatformDisplayNames,
|
||||
} from '../platform';
|
||||
import { maybeUploadFingerprintAsync } from '../project/maybeUploadFingerprintAsync';
|
||||
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
|
||||
import { uploadFileAtPathToGCSAsync } from '../uploads';
|
||||
import { formatBytes } from '../utils/files';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
@@ -95,7 +87,7 @@ interface Builder<TPlatform extends Platform, Credentials, TJob extends BuildJob
|
||||
sendBuildRequestAsync(
|
||||
appId: string,
|
||||
job: TJob,
|
||||
metadata: BuildMetadataInput,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult>;
|
||||
}
|
||||
@@ -132,9 +124,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
|
||||
await withAnalyticsAsync(
|
||||
ctx.analytics,
|
||||
async () => {
|
||||
await builder.syncProjectConfigurationAsync(ctx);
|
||||
},
|
||||
async () => await builder.syncProjectConfigurationAsync(ctx),
|
||||
{
|
||||
attemptEvent: BuildEvent.CONFIGURE_PROJECT_ATTEMPT,
|
||||
successEvent: BuildEvent.CONFIGURE_PROJECT_SUCCESS,
|
||||
@@ -145,15 +135,9 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
|
||||
if (await ctx.vcsClient.isCommitRequiredAsync()) {
|
||||
Log.addNewLineIfNone();
|
||||
const platformToRequestedPlatform: Record<Platform, RequestedPlatform> = {
|
||||
[Platform.ANDROID]: RequestedPlatform.Android,
|
||||
[Platform.IOS]: RequestedPlatform.Ios,
|
||||
};
|
||||
await reviewAndCommitChangesAsync(
|
||||
ctx.vcsClient,
|
||||
`[EAS Build] Run EAS Build for ${
|
||||
requestedPlatformDisplayNames[platformToRequestedPlatform[ctx.platform]]
|
||||
}`,
|
||||
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
|
||||
{ nonInteractive: ctx.nonInteractive }
|
||||
);
|
||||
}
|
||||
@@ -178,9 +162,7 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
}
|
||||
assert(projectArchive);
|
||||
|
||||
const runtimeAndFingerprintMetadata =
|
||||
await computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync(ctx);
|
||||
const metadata = await collectMetadataAsync(ctx, runtimeAndFingerprintMetadata);
|
||||
const metadata = await collectMetadataAsync(ctx);
|
||||
const buildParams = resolveBuildParamsInput(ctx, metadata);
|
||||
const job = await builder.prepareJobAsync(ctx, {
|
||||
projectArchive,
|
||||
@@ -189,25 +171,18 @@ export async function prepareBuildRequestForPlatformAsync<
|
||||
|
||||
return async () => {
|
||||
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions, ctx.env);
|
||||
await runLocalBuildAsync(job, metadata, ctx.localBuildOptions);
|
||||
return undefined;
|
||||
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
await BuildMutation.updateBuildMetadataAsync(ctx.graphqlClient, {
|
||||
buildId: nullthrows(process.env.EAS_BUILD_ID),
|
||||
metadata: transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
|
||||
}),
|
||||
metadata: transformMetadata(metadata),
|
||||
});
|
||||
printJsonOnlyOutput({ job, metadata });
|
||||
return undefined;
|
||||
} else if (!ctx.localBuildOptions.localBuildMode) {
|
||||
try {
|
||||
const graphqlMetadata = transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource: runtimeAndFingerprintMetadata.fingerprintSource ?? undefined,
|
||||
});
|
||||
return await sendBuildRequestAsync(builder, job, graphqlMetadata, buildParams);
|
||||
return await sendBuildRequestAsync(builder, job, metadata, buildParams);
|
||||
} catch (error: any) {
|
||||
handleBuildRequestError(error, job.platform);
|
||||
}
|
||||
@@ -285,27 +260,35 @@ async function uploadProjectAsync<TPlatform extends Platform>(
|
||||
);
|
||||
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
|
||||
|
||||
maybeWarnAboutProjectTarballSize(projectTarball.size);
|
||||
assertProjectTarballSizeDoesNotExceedLimit(projectTarball.size);
|
||||
if (projectTarball.size > 1024 * 1024 * 100) {
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
projectTarball.size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
|
||||
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
|
||||
}
|
||||
|
||||
projectTarballPath = projectTarball.path;
|
||||
const [bucketKey, { metadataLocation }] = await Promise.all([
|
||||
uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildGcsProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(
|
||||
projectTarball.size * ratio
|
||||
)} / ${formatBytes(projectTarball.size)})`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
})
|
||||
),
|
||||
uploadMetadataFileAsync<TPlatform>(projectTarball, ctx),
|
||||
]);
|
||||
|
||||
const bucketKey = await uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildGcsProjectSources,
|
||||
projectTarball.path,
|
||||
createProgressTracker({
|
||||
total: projectTarball.size,
|
||||
message: ratio =>
|
||||
`Uploading to EAS Build (${formatBytes(projectTarball.size * ratio)} / ${formatBytes(
|
||||
projectTarball.size
|
||||
)})`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
})
|
||||
);
|
||||
const { metadataLocation } = await uploadMetadataFileAsync<TPlatform>(projectTarball, ctx);
|
||||
if (metadataLocation) {
|
||||
return { bucketKey, metadataLocation };
|
||||
}
|
||||
@@ -344,7 +327,15 @@ async function uploadMetadataFileAsync<TPlatform extends Platform>(
|
||||
const metadataLocation = await uploadFileAtPathToGCSAsync(
|
||||
ctx.graphqlClient,
|
||||
UploadSessionType.EasBuildGcsProjectMetadata,
|
||||
projectMetadataFile.path
|
||||
projectMetadataFile.path,
|
||||
createProgressTracker({
|
||||
total: projectMetadataFile.size,
|
||||
message: ratio =>
|
||||
`Uploading metadata to EAS Build (${formatBytes(
|
||||
projectMetadataFile!.size * ratio
|
||||
)} / ${formatBytes(projectMetadataFile!.size)})`,
|
||||
completedMessage: (duration: string) => `Uploaded to EAS ${chalk.dim(duration)}`,
|
||||
})
|
||||
);
|
||||
return { metadataLocation };
|
||||
} catch (err: any) {
|
||||
@@ -370,7 +361,7 @@ async function sendBuildRequestAsync<
|
||||
>(
|
||||
builder: Builder<TPlatform, Credentials, TJob>,
|
||||
job: TJob,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildFragment> {
|
||||
const { ctx } = builder;
|
||||
@@ -384,7 +375,7 @@ async function sendBuildRequestAsync<
|
||||
const { build, deprecationInfo } = await builder.sendBuildRequestAsync(
|
||||
ctx.projectId,
|
||||
job,
|
||||
graphqlMetadata,
|
||||
metadata,
|
||||
buildParams
|
||||
);
|
||||
|
||||
@@ -487,8 +478,8 @@ async function handleSingleBuildProgressAsync(
|
||||
spinner.succeed('Build finished');
|
||||
return { refetch: false };
|
||||
case BuildStatus.New:
|
||||
spinner.text = `Build concurrency limit reached for your account. Build will enter queue once a concurrency becomes available. Add additional concurrencies at ${link(
|
||||
formatAccountBillingUrl(accountName)
|
||||
spinner.text = `Build is waiting to enter the queue. Check your concurrency limit at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}.`;
|
||||
break;
|
||||
case BuildStatus.InQueue: {
|
||||
@@ -508,7 +499,11 @@ async function handleSingleBuildProgressAsync(
|
||||
if (build.priority !== BuildPriority.High) {
|
||||
Log.newLine();
|
||||
Log.log('Start builds sooner in the priority queue.');
|
||||
Log.log(`Sign up for a paid plan at ${link(formatAccountBillingUrl(accountName))}`);
|
||||
Log.log(
|
||||
`Sign up for EAS Production or Enterprise at ${link(
|
||||
formatAccountSubscriptionsUrl(accountName)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
@@ -653,105 +648,9 @@ function formatEstimatedWaitTime(estimatedWaitTimeLeftSeconds: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
function formatAccountBillingUrl(accountName: string): string {
|
||||
return new URL(`/accounts/${accountName}/settings/billing`, getExpoWebsiteBaseUrl()).toString();
|
||||
}
|
||||
|
||||
async function computeAndMaybeUploadRuntimeAndFingerprintMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
fingerprintSource?: FingerprintSourceInput | 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?: FingerprintSourceInput;
|
||||
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?: FingerprintSourceInput;
|
||||
}> {
|
||||
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,
|
||||
};
|
||||
function formatAccountSubscriptionsUrl(accountName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${accountName}/settings/subscriptions`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
import { AppVersionSource, EasJson, EasJsonAccessor } from '@expo/eas-json';
|
||||
import { EasJson, EasJsonAccessor } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
|
||||
import Log, { learnMore } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
@@ -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 = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
@@ -138,9 +64,7 @@ const EAS_JSON_DEFAULT: EasJson = {
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
},
|
||||
production: {
|
||||
autoIncrement: true,
|
||||
},
|
||||
production: {},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
|
||||
@@ -33,7 +33,6 @@ export interface IosBuildContext {
|
||||
}
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -63,7 +62,4 @@ export interface BuildContext<T extends Platform> {
|
||||
requiredPackageManager: NodePackageManager['name'] | null;
|
||||
vcsClient: Client;
|
||||
loggerLevel?: LoggerLevel;
|
||||
isVerboseLoggingEnabled: boolean;
|
||||
whatToTest?: string;
|
||||
env: Record<string, string>;
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { createAndroidContextAsync } from './android/build';
|
||||
import { BuildContext, CommonContext } from './context';
|
||||
import { createIosContextAsync } from './ios/build';
|
||||
import { LocalBuildMode, LocalBuildOptions } from './local';
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { resolveBuildResourceClassAsync } from './utils/resourceClass';
|
||||
import { Analytics, AnalyticsEventProperties, BuildEvent } from '../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
|
||||
@@ -41,10 +41,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel,
|
||||
freezeCredentials,
|
||||
isVerboseLoggingEnabled,
|
||||
whatToTest,
|
||||
env,
|
||||
}: {
|
||||
buildProfileName: string;
|
||||
buildProfile: BuildProfile<T>;
|
||||
@@ -64,14 +60,8 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
isVerboseLoggingEnabled: boolean;
|
||||
whatToTest?: string;
|
||||
env: Record<string, string>;
|
||||
}): Promise<BuildContext<T>> {
|
||||
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
|
||||
env,
|
||||
});
|
||||
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
|
||||
const projectName = exp.slug;
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
|
||||
@@ -93,10 +83,9 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
user: actor,
|
||||
graphqlClient,
|
||||
analytics,
|
||||
env,
|
||||
env: buildProfile.env,
|
||||
easJsonCliConfig,
|
||||
vcsClient,
|
||||
freezeCredentials,
|
||||
});
|
||||
|
||||
const devClientProperties = getDevClientEventProperties({
|
||||
@@ -114,7 +103,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
...devClientProperties,
|
||||
no_wait: noWait,
|
||||
run_from_ci: runFromCI,
|
||||
local: localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN,
|
||||
};
|
||||
analytics.logEvent(BuildEvent.BUILD_COMMAND, analyticsEventProperties);
|
||||
|
||||
@@ -125,7 +113,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountId,
|
||||
accountName: account.name,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
@@ -153,9 +140,6 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
developmentClient,
|
||||
requiredPackageManager,
|
||||
loggerLevel: buildLoggerLevel,
|
||||
isVerboseLoggingEnabled,
|
||||
env,
|
||||
whatToTest,
|
||||
};
|
||||
if (platform === Platform.ANDROID) {
|
||||
const common = commonContext as CommonContext<Platform.ANDROID>;
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { DefaultEnvironment } from './utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
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 ??
|
||||
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
|
||||
|
||||
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
|
||||
): DefaultEnvironment {
|
||||
const environment =
|
||||
buildProfile.distribution === 'store'
|
||||
? DefaultEnvironment.Production
|
||||
: buildProfile.developmentClient
|
||||
? DefaultEnvironment.Development
|
||||
: DefaultEnvironment.Preview;
|
||||
|
||||
Log.log(
|
||||
`Resolved "${environment}" environment for the build. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
|
||||
)}`
|
||||
);
|
||||
|
||||
return environment;
|
||||
}
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
BuildMetadataInput,
|
||||
BuildWorkflow,
|
||||
DistributionType,
|
||||
FingerprintSourceInput,
|
||||
BuildMode as GraphQLBuildMode,
|
||||
BuildTrigger as GraphQLBuildTrigger,
|
||||
ProjectArchiveSourceInput,
|
||||
@@ -40,16 +39,9 @@ export function transformProjectArchive(archiveSource: ArchiveSource): ProjectAr
|
||||
}
|
||||
}
|
||||
|
||||
export function transformMetadata({
|
||||
metadata,
|
||||
fingerprintSource,
|
||||
}: {
|
||||
metadata: Metadata;
|
||||
fingerprintSource: FingerprintSourceInput | undefined;
|
||||
}): BuildMetadataInput {
|
||||
export function transformMetadata(metadata: Metadata): BuildMetadataInput {
|
||||
return {
|
||||
...metadata,
|
||||
fingerprintSource,
|
||||
credentialsSource:
|
||||
metadata.credentialsSource && transformCredentialsSource(metadata.credentialsSource),
|
||||
distribution: metadata.distribution && transformDistribution(metadata.distribution),
|
||||
@@ -102,7 +94,6 @@ const buildModeToGraphQLBuildMode: Record<BuildMode, GraphQLBuildMode> = {
|
||||
[BuildMode.BUILD]: GraphQLBuildMode.Build,
|
||||
[BuildMode.CUSTOM]: GraphQLBuildMode.Custom,
|
||||
[BuildMode.RESIGN]: GraphQLBuildMode.Resign,
|
||||
[BuildMode.REPACK]: GraphQLBuildMode.Repack,
|
||||
};
|
||||
|
||||
export function transformBuildMode(buildMode: BuildMode): GraphQLBuildMode {
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { IOSConfig } from '@expo/config-plugins';
|
||||
import { IosBuildProfile } from '@expo/eas-json/build/build/types';
|
||||
import fs from 'fs-extra';
|
||||
import { vol } from 'memfs';
|
||||
import os from 'os';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { Target } from '../../../credentials/ios/types';
|
||||
import { AppVersionMutation } from '../../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../../graphql/queries/AppVersionQuery';
|
||||
import { readPlistAsync } from '../../../utils/plist';
|
||||
import { resolveVcsClient } from '../../../vcs';
|
||||
import { Client } from '../../../vcs/vcs';
|
||||
import {
|
||||
BumpStrategy,
|
||||
bumpVersionAsync,
|
||||
@@ -22,23 +15,9 @@ import {
|
||||
getInfoPlistPath,
|
||||
readBuildNumberAsync,
|
||||
readShortVersionAsync,
|
||||
resolveRemoteBuildNumberAsync,
|
||||
} from '../version';
|
||||
|
||||
jest.mock('fs');
|
||||
jest.mock('../../../commandUtils/context/contextUtils/createGraphqlClient');
|
||||
jest.mock('../../../vcs/vcs');
|
||||
jest.mock('../../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../../graphql/mutations/AppVersionMutation');
|
||||
jest.mock('../../../ora', () => ({
|
||||
ora: jest.fn().mockImplementation(() => ({
|
||||
start: jest.fn().mockImplementation(() => ({
|
||||
succeed: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
fail: jest.fn(),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
|
||||
const vcsClient = resolveVcsClient();
|
||||
|
||||
@@ -321,6 +300,25 @@ describe(readShortVersionAsync, () => {
|
||||
);
|
||||
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', () => {
|
||||
@@ -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({
|
||||
appVersion = '1.0.0',
|
||||
buildNumber = '1',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Ios, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Ios, Metadata, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { AppVersionSource } from '@expo/eas-json';
|
||||
|
||||
import { ensureIosCredentialsAsync } from './credentials';
|
||||
@@ -7,14 +7,14 @@ import { prepareJobAsync } from './prepareJob';
|
||||
import { syncProjectConfigurationAsync } from './syncProjectConfiguration';
|
||||
import { resolveRemoteBuildNumberAsync } from './version';
|
||||
import { IosCredentials } from '../../credentials/ios/types';
|
||||
import { BuildMetadataInput, BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildParamsInput } from '../../graphql/generated';
|
||||
import { BuildMutation, BuildResult } from '../../graphql/mutations/BuildMutation';
|
||||
import { ensureBundleIdentifierIsDefinedForManagedProjectAsync } from '../../project/ios/bundleIdentifier';
|
||||
import { ensureNonExemptEncryptionIsDefinedForManagedProjectAsync } from '../../project/ios/exemptEncryption';
|
||||
import { resolveXcodeBuildContextAsync } from '../../project/ios/scheme';
|
||||
import { findApplicationTarget, resolveTargetsAsync } from '../../project/ios/target';
|
||||
import { BuildRequestSender, JobData, prepareBuildRequestForPlatformAsync } from '../build';
|
||||
import { BuildContext, CommonContext, IosBuildContext } from '../context';
|
||||
import { transformMetadata } from '../graphql';
|
||||
import {
|
||||
checkGoogleServicesFileAsync,
|
||||
checkNodeEnvVariable,
|
||||
@@ -24,11 +24,10 @@ import {
|
||||
export async function createIosContextAsync(
|
||||
ctx: CommonContext<Platform.IOS>
|
||||
): Promise<IosBuildContext> {
|
||||
const { buildProfile, env } = ctx;
|
||||
const { buildProfile } = ctx;
|
||||
|
||||
if (ctx.workflow === Workflow.MANAGED) {
|
||||
await ensureBundleIdentifierIsDefinedForManagedProjectAsync(ctx);
|
||||
await ensureNonExemptEncryptionIsDefinedForManagedProjectAsync(ctx);
|
||||
}
|
||||
|
||||
checkNodeEnvVariable(ctx);
|
||||
@@ -48,7 +47,7 @@ export async function createIosContextAsync(
|
||||
projectDir: ctx.projectDir,
|
||||
exp: ctx.exp,
|
||||
xcodeBuildContext,
|
||||
env,
|
||||
env: buildProfile.env,
|
||||
vcsClient: ctx.vcsClient,
|
||||
});
|
||||
const applicationTarget = findApplicationTarget(targets);
|
||||
@@ -78,7 +77,7 @@ export async function prepareIosBuildAsync(
|
||||
return await prepareBuildRequestForPlatformAsync({
|
||||
ctx,
|
||||
ensureCredentialsAsync: async (ctx: BuildContext<Platform.IOS>) => {
|
||||
return await ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
return ensureIosCredentialsAsync(ctx, ctx.ios.targets);
|
||||
},
|
||||
syncProjectConfigurationAsync: async () => {
|
||||
await syncProjectConfigurationAsync({
|
||||
@@ -90,7 +89,6 @@ export async function prepareIosBuildAsync(
|
||||
? false
|
||||
: ctx.buildProfile.autoIncrement,
|
||||
vcsClient: ctx.vcsClient,
|
||||
env: ctx.env,
|
||||
});
|
||||
},
|
||||
prepareJobAsync: async (
|
||||
@@ -105,9 +103,10 @@ export async function prepareIosBuildAsync(
|
||||
sendBuildRequestAsync: async (
|
||||
appId: string,
|
||||
job: Ios.Job,
|
||||
graphqlMetadata: BuildMetadataInput,
|
||||
metadata: Metadata,
|
||||
buildParams: BuildParamsInput
|
||||
): Promise<BuildResult> => {
|
||||
const graphqlMetadata = transformMetadata(metadata);
|
||||
const graphqlJob = transformJob(job);
|
||||
return await BuildMutation.createIosBuildAsync(ctx.graphqlClient, {
|
||||
appId,
|
||||
|
||||
@@ -16,6 +16,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
triggeredBy: transformBuildTrigger(job.triggeredBy),
|
||||
projectArchive: transformProjectArchive(job.projectArchive),
|
||||
projectRootDirectory: nullthrows(job.projectRootDirectory),
|
||||
releaseChannel: job.releaseChannel,
|
||||
updates: job.updates,
|
||||
secrets: job.secrets ? transformIosSecrets(job.secrets) : undefined,
|
||||
builderEnvironment: job.builderEnvironment,
|
||||
@@ -31,7 +32,6 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -14,7 +14,7 @@ import slash from 'slash';
|
||||
import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
|
||||
import { IosJobSecretsInput } from '../../graphql/generated';
|
||||
import { getCustomBuildConfigPathForJob } from '../../project/customBuildConfig';
|
||||
import { getUsernameForBuildMetadataAndBuildJob } from '../../project/projectUtils';
|
||||
import { getUsername } from '../../project/projectUtils';
|
||||
import { BuildContext } from '../context';
|
||||
|
||||
interface JobData {
|
||||
@@ -32,7 +32,7 @@ export async function prepareJobAsync(
|
||||
ctx: BuildContext<Platform.IOS>,
|
||||
jobData: JobData
|
||||
): Promise<Ios.Job> {
|
||||
const username = getUsernameForBuildMetadataAndBuildJob(ctx.user);
|
||||
const username = getUsername(ctx.exp, ctx.user);
|
||||
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
|
||||
const projectRootDirectory =
|
||||
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
|
||||
@@ -48,13 +48,6 @@ export async function prepareJobAsync(
|
||||
? getCustomBuildConfigPathForJob(buildProfile.config)
|
||||
: undefined;
|
||||
|
||||
let buildMode;
|
||||
if (buildProfile.config) {
|
||||
buildMode = BuildMode.CUSTOM;
|
||||
} else {
|
||||
buildMode = BuildMode.BUILD;
|
||||
}
|
||||
|
||||
const job: Ios.Job = {
|
||||
type: ctx.workflow,
|
||||
platform: Platform.IOS,
|
||||
@@ -63,17 +56,14 @@ export async function prepareJobAsync(
|
||||
builderEnvironment: {
|
||||
image: buildProfile.image,
|
||||
node: buildProfile.node,
|
||||
corepack: buildProfile.corepack,
|
||||
pnpm: buildProfile.pnpm,
|
||||
bun: buildProfile.bun,
|
||||
yarn: buildProfile.yarn,
|
||||
bundler: buildProfile.bundler,
|
||||
cocoapods: buildProfile.cocoapods,
|
||||
fastlane: buildProfile.fastlane,
|
||||
env: {
|
||||
...buildProfile.env,
|
||||
...(ctx.isVerboseLoggingEnabled ? { EAS_VERBOSE: '1' } : {}),
|
||||
},
|
||||
expoCli: buildProfile.expoCli,
|
||||
env: buildProfile.env,
|
||||
},
|
||||
cache: {
|
||||
...cacheDefaults,
|
||||
@@ -83,6 +73,7 @@ export async function prepareJobAsync(
|
||||
secrets: {
|
||||
buildCredentials,
|
||||
},
|
||||
releaseChannel: buildProfile.releaseChannel,
|
||||
updates: { channel: buildProfile.channel },
|
||||
developmentClient: buildProfile.developmentClient,
|
||||
simulator: buildProfile.simulator,
|
||||
@@ -90,7 +81,6 @@ export async function prepareJobAsync(
|
||||
buildConfiguration: buildProfile.buildConfiguration,
|
||||
applicationArchivePath: buildProfile.applicationArchivePath ?? buildProfile.artifactPath,
|
||||
buildArtifactPaths: buildProfile.buildArtifactPaths,
|
||||
environment: ctx.buildProfile.environment,
|
||||
username,
|
||||
...(ctx.ios.buildNumberOverride && {
|
||||
version: {
|
||||
@@ -100,7 +90,7 @@ export async function prepareJobAsync(
|
||||
experimental: {
|
||||
prebuildCommand: buildProfile.prebuildCommand,
|
||||
},
|
||||
mode: buildMode,
|
||||
mode: buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
|
||||
triggeredBy: BuildTrigger.EAS_CLI,
|
||||
...(maybeCustomBuildConfigPath && {
|
||||
customBuildConfig: {
|
||||
@@ -108,10 +98,6 @@ export async function prepareJobAsync(
|
||||
},
|
||||
}),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { IosVersionAutoIncrement } from '@expo/eas-json';
|
||||
|
||||
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
|
||||
@@ -15,21 +15,19 @@ export async function syncProjectConfigurationAsync({
|
||||
targets,
|
||||
localAutoIncrement,
|
||||
vcsClient,
|
||||
env,
|
||||
}: {
|
||||
projectDir: string;
|
||||
exp: ExpoConfig;
|
||||
targets: Target[];
|
||||
localAutoIncrement?: IosVersionAutoIncrement;
|
||||
vcsClient: Client;
|
||||
env: Env | undefined;
|
||||
}): Promise<void> {
|
||||
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
|
||||
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
|
||||
|
||||
if (workflow === Workflow.GENERIC) {
|
||||
if (isExpoUpdatesInstalled(projectDir)) {
|
||||
await syncUpdatesConfigurationAsync({ vcsClient, projectDir, exp, workflow, env });
|
||||
await syncUpdatesConfigurationAsync(vcsClient, projectDir, exp);
|
||||
}
|
||||
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
|
||||
} else {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Target } from '../../credentials/ios/types';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { AppVersionMutation } from '../../graphql/mutations/AppVersionMutation';
|
||||
import { AppVersionQuery } from '../../graphql/queries/AppVersionQuery';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { findApplicationTarget } from '../../project/ios/target';
|
||||
import { getNextBuildNumber, isValidBuildNumber } from '../../project/ios/versions';
|
||||
@@ -23,7 +23,7 @@ import { Client } from '../../vcs/vcs';
|
||||
import { updateAppJsonConfigAsync } from '../utils/appJson';
|
||||
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
|
||||
|
||||
const SHORT_VERSION_REGEX = /^\d+(\.\d+){0,2}$/;
|
||||
const SHORT_VERSION_REGEX = /^\d+.\d+.\d+$/;
|
||||
|
||||
export enum BumpStrategy {
|
||||
APP_VERSION,
|
||||
@@ -100,27 +100,11 @@ export async function bumpVersionInAppJsonAsync({
|
||||
}
|
||||
}
|
||||
|
||||
function validateShortVersion({
|
||||
shortVersion,
|
||||
workflow,
|
||||
}: {
|
||||
shortVersion: string | undefined;
|
||||
workflow: Workflow;
|
||||
}): void {
|
||||
function validateShortVersion(shortVersion: string | undefined): void {
|
||||
if (shortVersion && !SHORT_VERSION_REGEX.test(shortVersion)) {
|
||||
if (workflow === Workflow.MANAGED) {
|
||||
Log.warn(
|
||||
`The required format for "version" field from app.json/app.config.ts for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "version" field in your app.json/app.config.ts to match the format required by Apple. ${learnMore(
|
||||
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
Log.warn(
|
||||
`The required format for "CFBundleShortVersionString" in Info.plist for App Store iOS builds is one to three period-separated integers, such as 10.14.1. The string can only contain numeric characters (0-9) and periods. Current value: ${shortVersion}. Edit the "CFBundleShortVersionString" in your Info.plist to match the format required by Apple. ${learnMore(
|
||||
'https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleshortversionstring'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: ${shortVersion}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,10 +122,10 @@ export async function readShortVersionAsync(
|
||||
infoPlist.CFBundleShortVersionString &&
|
||||
evaluateTemplateString(infoPlist.CFBundleShortVersionString, buildSettings);
|
||||
|
||||
validateShortVersion({ shortVersion, workflow });
|
||||
validateShortVersion(shortVersion);
|
||||
return shortVersion;
|
||||
} else {
|
||||
validateShortVersion({ shortVersion: exp.version, workflow });
|
||||
validateShortVersion(exp.version);
|
||||
return exp.version;
|
||||
}
|
||||
}
|
||||
@@ -330,18 +314,14 @@ export async function resolveRemoteBuildNumberAsync(
|
||||
vcsClient
|
||||
);
|
||||
let currentBuildVersion: string;
|
||||
let shouldInitializeBuildNumber = false;
|
||||
if (remoteVersions?.buildVersion) {
|
||||
currentBuildVersion = remoteVersions.buildVersion;
|
||||
} else {
|
||||
if (localBuildNumber) {
|
||||
Log.log(
|
||||
chalk.green(
|
||||
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
|
||||
)
|
||||
Log.warn(
|
||||
'No remote versions are configured for this project, buildNumber will be initialized based on the value from the local project.'
|
||||
);
|
||||
currentBuildVersion = localBuildNumber;
|
||||
shouldInitializeBuildNumber = localBuildNumber === '1';
|
||||
} else {
|
||||
Log.error(
|
||||
`Remote versions are not configured and EAS CLI was not able to read the current version from your project. Use "eas build:version:set" to initialize remote versions.`
|
||||
@@ -351,10 +331,7 @@ export async function resolveRemoteBuildNumberAsync(
|
||||
}
|
||||
if (!buildProfile.autoIncrement && remoteVersions?.buildVersion) {
|
||||
return currentBuildVersion;
|
||||
} else if (
|
||||
(!buildProfile.autoIncrement && !remoteVersions?.buildVersion) ||
|
||||
shouldInitializeBuildNumber
|
||||
) {
|
||||
} else if (!buildProfile.autoIncrement && !remoteVersions?.buildVersion) {
|
||||
const spinner = ora(
|
||||
`Initializing buildNumber with ${chalk.bold(currentBuildVersion)}.`
|
||||
).start();
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { Job, Metadata, version } from '@expo/eas-build-job';
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import { ChildProcess } from 'child_process';
|
||||
import semver from 'semver';
|
||||
|
||||
import { getExpoApiBaseUrl } from '../api';
|
||||
import Log from '../log';
|
||||
import { ora } from '../ora';
|
||||
|
||||
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
|
||||
const PLUGIN_PACKAGE_VERSION = version; // should match version of @expo/eas-build-job
|
||||
const PLUGIN_PACKAGE_VERSION = '1.0.100';
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
@@ -41,8 +39,7 @@ export interface LocalBuildOptions {
|
||||
export async function runLocalBuildAsync(
|
||||
job: Job,
|
||||
metadata: Metadata,
|
||||
options: LocalBuildOptions,
|
||||
env: Record<string, string>
|
||||
options: LocalBuildOptions
|
||||
): Promise<void> {
|
||||
const { command, args } = await getCommandAndArgsAsync(job, metadata);
|
||||
let spinner;
|
||||
@@ -57,27 +54,18 @@ export async function runLocalBuildAsync(
|
||||
};
|
||||
process.on('SIGINT', interruptHandler);
|
||||
try {
|
||||
const mergedEnv = {
|
||||
...env,
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
__API_SERVER_URL: getExpoApiBaseUrl(),
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
|
||||
};
|
||||
// log command execution to assist in debugging local builds
|
||||
Log.debug('Running local build, using local-build-plugin', {
|
||||
command,
|
||||
args,
|
||||
env: mergedEnv,
|
||||
});
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: options.verbose ? 'inherit' : 'pipe',
|
||||
env: mergedEnv,
|
||||
env: {
|
||||
...process.env,
|
||||
EAS_LOCAL_BUILD_WORKINGDIR: options.workingdir ?? process.env.EAS_LOCAL_BUILD_WORKINGDIR,
|
||||
...(options.skipCleanup || options.skipNativeBuild
|
||||
? { EAS_LOCAL_BUILD_SKIP_CLEANUP: '1' }
|
||||
: {}),
|
||||
...(options.skipNativeBuild ? { EAS_LOCAL_BUILD_SKIP_NATIVE_BUILD: '1' } : {}),
|
||||
...(options.artifactsDir ? { EAS_LOCAL_BUILD_ARTIFACTS_DIR: options.artifactsDir } : {}),
|
||||
...(options.artifactPath ? { EAS_LOCAL_BUILD_ARTIFACT_PATH: options.artifactPath } : {}),
|
||||
},
|
||||
});
|
||||
childProcess = spawnPromise.child;
|
||||
await spawnPromise;
|
||||
|
||||
@@ -10,21 +10,25 @@ import { LocalBuildMode } from './local';
|
||||
import { BuildDistributionType } from './types';
|
||||
import Log from '../log';
|
||||
import {
|
||||
getUsernameForBuildMetadataAndBuildJob,
|
||||
getUsername,
|
||||
isClassicUpdatesSupportedAsync,
|
||||
isExpoUpdatesInstalled,
|
||||
} from '../project/projectUtils';
|
||||
import { readChannelSafelyAsync as readAndroidChannelSafelyAsync } from '../update/android/UpdatesModule';
|
||||
import { readChannelSafelyAsync as readIosChannelSafelyAsync } from '../update/ios/UpdatesModule';
|
||||
import { resolveRuntimeVersionAsync } from '../project/resolveRuntimeVersionAsync';
|
||||
import {
|
||||
readChannelSafelyAsync as readAndroidChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readAndroidReleaseChannelSafelyAsync,
|
||||
} from '../update/android/UpdatesModule';
|
||||
import {
|
||||
readChannelSafelyAsync as readIosChannelSafelyAsync,
|
||||
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
|
||||
} from '../update/ios/UpdatesModule';
|
||||
import { easCliVersion } from '../utils/easCli';
|
||||
|
||||
export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>,
|
||||
runtimeAndFingerprintMetadata: {
|
||||
runtimeVersion?: string | undefined;
|
||||
fingerprintHash?: string | undefined;
|
||||
}
|
||||
ctx: BuildContext<T>
|
||||
): Promise<Metadata> {
|
||||
const channelObject = await resolveChannelAsync(ctx);
|
||||
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
|
||||
const distribution = ctx.buildProfile.distribution ?? BuildDistributionType.STORE;
|
||||
const metadata: Metadata = {
|
||||
trackingContext: ctx.analyticsEventProperties,
|
||||
@@ -33,10 +37,9 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
workflow: ctx.workflow,
|
||||
credentialsSource: ctx.buildProfile.credentialsSource,
|
||||
sdkVersion: ctx.exp.sdkVersion,
|
||||
runtimeVersion: runtimeAndFingerprintMetadata?.runtimeVersion,
|
||||
fingerprintHash: runtimeAndFingerprintMetadata?.fingerprintHash,
|
||||
runtimeVersion: (await resolveRuntimeVersionAsync(ctx)) ?? undefined,
|
||||
reactNativeVersion: await getReactNativeVersionAsync(ctx.projectDir),
|
||||
...channelObject,
|
||||
...channelOrReleaseChannel,
|
||||
distribution,
|
||||
appName: ctx.exp.name,
|
||||
appIdentifier: resolveAppIdentifier(ctx),
|
||||
@@ -49,7 +52,7 @@ export async function collectMetadataAsync<T extends Platform>(
|
||||
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
|
||||
? false
|
||||
: await ctx.vcsClient.hasUncommittedChangesAsync(),
|
||||
username: getUsernameForBuildMetadataAndBuildJob(ctx.user),
|
||||
username: getUsername(ctx.exp, ctx.user),
|
||||
message: ctx.message,
|
||||
...(ctx.platform === Platform.IOS && {
|
||||
iosEnterpriseProvisioning: resolveIosEnterpriseProvisioning(
|
||||
@@ -114,9 +117,9 @@ function resolveAppIdentifier<T extends Platform>(ctx: BuildContext<T>): string
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveChannelAsync<T extends Platform>(
|
||||
async function resolveChannelOrReleaseChannelAsync<T extends Platform>(
|
||||
ctx: BuildContext<T>
|
||||
): Promise<{ channel: string } | null> {
|
||||
): Promise<{ channel: string } | { releaseChannel: string } | null> {
|
||||
if (!isExpoUpdatesInstalled(ctx.projectDir)) {
|
||||
return null;
|
||||
}
|
||||
@@ -128,7 +131,31 @@ async function resolveChannelAsync<T extends Platform>(
|
||||
return { channel };
|
||||
}
|
||||
|
||||
return null;
|
||||
if (!(await isClassicUpdatesSupportedAsync(ctx.projectDir))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ctx.buildProfile.releaseChannel) {
|
||||
return { releaseChannel: ctx.buildProfile.releaseChannel };
|
||||
}
|
||||
|
||||
const releaseChannel = await getNativeReleaseChannelAsync(ctx);
|
||||
return { releaseChannel };
|
||||
}
|
||||
|
||||
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>(
|
||||
|
||||
@@ -50,9 +50,8 @@ export async function listAndRenderBuildsOnAppAsync(
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more builds?',
|
||||
renderListItems: builds => {
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions });
|
||||
},
|
||||
renderListItems: builds =>
|
||||
renderPageOfBuilds({ builds, projectDisplayName, paginatedQueryOptions }),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { Env, Platform, Workflow } from '@expo/eas-build-job';
|
||||
import { ExpoConfig } from '@expo/config-types';
|
||||
import { Platform, Workflow } from '@expo/eas-build-job';
|
||||
import {
|
||||
AppVersionSource,
|
||||
BuildProfile,
|
||||
EasJson,
|
||||
EasJsonAccessor,
|
||||
EasJsonUtils,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
import { pathExists } from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { prepareAndroidBuildAsync } from './android/build';
|
||||
@@ -19,7 +19,6 @@ import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './
|
||||
import { ensureProjectConfiguredAsync } from './configure';
|
||||
import { BuildContext } from './context';
|
||||
import { createBuildContextAsync } from './createContext';
|
||||
import { evaluateConfigWithEnvVarsAsync } from './evaluateConfigWithEnvVarsAsync';
|
||||
import { prepareIosBuildAsync } from './ios/build';
|
||||
import { LocalBuildMode, LocalBuildOptions } from './local';
|
||||
import { ensureExpoDevClientInstalledForDevClientBuildsAsync } from './utils/devClient';
|
||||
@@ -58,12 +57,11 @@ import {
|
||||
validateAppVersionRuntimePolicySupportAsync,
|
||||
} from '../project/projectUtils';
|
||||
import {
|
||||
ensureAppVersionSourceIsSetAsync,
|
||||
validateAppConfigForRemoteVersionSource,
|
||||
validateBuildProfileVersionSettingsAsync,
|
||||
validateBuildProfileVersionSettings,
|
||||
} from '../project/remoteVersionSource';
|
||||
import { confirmAsync } from '../prompts';
|
||||
import { getEasBuildRunCachedAppPath, runAsync } from '../run/run';
|
||||
import { runAsync } from '../run/run';
|
||||
import { isRunnableOnSimulatorOrEmulator } from '../run/utils';
|
||||
import { createSubmissionContextAsync } from '../submit/context';
|
||||
import {
|
||||
@@ -73,17 +71,16 @@ import {
|
||||
} from '../submit/submit';
|
||||
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
|
||||
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
|
||||
import { Actor } from '../user/User';
|
||||
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
|
||||
import { truthy } from '../utils/expodash/filter';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import { ProfileData, getProfilesAsync } from '../utils/profiles';
|
||||
import { maybeWarnAboutUsageOveragesAsync } from '../utils/usage/checkForOverages';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
let metroConfigValidated = false;
|
||||
let sdkVersionChecked = false;
|
||||
let hasWarnedAboutUsageOverages = false;
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
@@ -98,35 +95,17 @@ export interface BuildFlags {
|
||||
resourceClass?: ResourceClass;
|
||||
message?: string;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
isVerboseLoggingEnabled?: boolean;
|
||||
whatToTest?: string;
|
||||
}
|
||||
|
||||
export async function runBuildAndSubmitAsync({
|
||||
graphqlClient,
|
||||
analytics,
|
||||
vcsClient,
|
||||
projectDir,
|
||||
flags,
|
||||
actor,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
downloadSimBuildAutoConfirm,
|
||||
envOverride,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
analytics: Analytics;
|
||||
vcsClient: Client;
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
actor: Actor;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
downloadSimBuildAutoConfirm?: boolean;
|
||||
envOverride?: Env;
|
||||
}): Promise<{
|
||||
buildIds: string[];
|
||||
buildProfiles?: ProfileData<'build'>[];
|
||||
}> {
|
||||
export async function runBuildAndSubmitAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
analytics: Analytics,
|
||||
vcsClient: Client,
|
||||
projectDir: string,
|
||||
flags: BuildFlags,
|
||||
actor: Actor,
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
|
||||
): Promise<void> {
|
||||
await vcsClient.ensureRepoExistsAsync();
|
||||
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
|
||||
|
||||
@@ -147,28 +126,39 @@ export async function runBuildAndSubmitAsync({
|
||||
profileName: flags.profile ?? undefined,
|
||||
projectDir,
|
||||
});
|
||||
Log.log(
|
||||
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
|
||||
buildProfiles[0].profile.env
|
||||
? Object.keys(buildProfiles[0].profile.env).join(', ')
|
||||
: 'no environment variables specified'
|
||||
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
|
||||
);
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
if (buildProfile.profile.image && ['default', 'stable'].includes(buildProfile.profile.image)) {
|
||||
Log.warn(
|
||||
`The "image" field in the build profile "${buildProfile.profileName}" is set to "${buildProfile.profile.image}". This tag is deprecated and will be removed in the future. Use other images or tags listed here: https://docs.expo.dev/build-reference/infrastructure/`
|
||||
);
|
||||
} else if (
|
||||
}
|
||||
|
||||
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',
|
||||
'macos-monterey-12.4-xcode-13.4',
|
||||
'macos-monterey-12.3-xcode-13.3',
|
||||
'macos-monterey-12.1-xcode-13.2',
|
||||
'macos-ventura-13.4-xcode-14.3.1',
|
||||
'macos-ventura-13.3-xcode-14.3',
|
||||
'macos-monterey-12.6-xcode-14.2',
|
||||
'macos-monterey-12.6-xcode-14.1',
|
||||
'macos-monterey-12.6-xcode-14.0',
|
||||
].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'
|
||||
}". This image is using Xcode in version lower then 15. Apple's new requirement states that starting on April 29th, 2024, apps uploaded to App Store Connect must be built with Xcode 15 for iOS 17, iPadOS 17, tvOS 17, or watchOS 10. Start using Xcode 15 image today by setting "macos-sonoma-14.4-xcode-15.3" or "latest" as your image value. This image will be removed from EAS Build on May 29th, 2024. ${learnMore(
|
||||
'https://expo.dev/changelog/2024/03-29-eas-build-upcoming-ios-images-updates'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
@@ -184,12 +174,7 @@ export async function runBuildAndSubmitAsync({
|
||||
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
|
||||
{};
|
||||
for (const buildProfile of buildProfiles) {
|
||||
await validateBuildProfileVersionSettingsAsync(
|
||||
buildProfile,
|
||||
easJsonCliConfig,
|
||||
projectDir,
|
||||
flags
|
||||
);
|
||||
validateBuildProfileVersionSettings(buildProfile, easJsonCliConfig);
|
||||
const maybeMetadata = await validateCustomBuildConfigAsync({
|
||||
projectDir,
|
||||
profile: buildProfile.profile,
|
||||
@@ -208,17 +193,6 @@ export async function runBuildAndSubmitAsync({
|
||||
|
||||
for (const buildProfile of buildProfiles) {
|
||||
const platform = toAppPlatform(buildProfile.platform);
|
||||
|
||||
const { env } = !envOverride
|
||||
? await evaluateConfigWithEnvVarsAsync({
|
||||
buildProfile: buildProfile.profile,
|
||||
buildProfileName: buildProfile.profileName,
|
||||
graphqlClient,
|
||||
getProjectConfig: getDynamicPrivateProjectConfigAsync,
|
||||
opts: { env: buildProfile.profile.env },
|
||||
})
|
||||
: { env: envOverride };
|
||||
|
||||
const { build: maybeBuild, buildCtx } = await prepareAndStartBuildAsync({
|
||||
projectDir,
|
||||
flags,
|
||||
@@ -231,8 +205,6 @@ export async function runBuildAndSubmitAsync({
|
||||
vcsClient,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
|
||||
env,
|
||||
easJsonAccessor,
|
||||
});
|
||||
if (maybeBuild) {
|
||||
startedBuilds.push({ build: maybeBuild, buildProfile });
|
||||
@@ -241,9 +213,7 @@ export async function runBuildAndSubmitAsync({
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
|
||||
return {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
|
||||
@@ -275,14 +245,14 @@ export async function runBuildAndSubmitAsync({
|
||||
({ platform }) => toAppPlatform(platform) === startedBuild.build.platform
|
||||
)
|
||||
).profile;
|
||||
const submission = await prepareAndStartAutoSubmissionAsync({
|
||||
const submission = await prepareAndStartSubmissionAsync({
|
||||
build: startedBuild.build,
|
||||
buildCtx: nullthrows(buildCtxByPlatform[startedBuild.build.platform]),
|
||||
moreBuilds: startedBuilds.length > 1,
|
||||
projectDir,
|
||||
buildProfile: startedBuild.buildProfile.profile,
|
||||
submitProfile,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
selectedSubmitProfileName: flags.submitProfile,
|
||||
});
|
||||
startedBuild.build = await BuildQuery.withSubmissionsByIdAsync(
|
||||
graphqlClient,
|
||||
@@ -299,18 +269,14 @@ export async function runBuildAndSubmitAsync({
|
||||
}
|
||||
|
||||
if (flags.localBuildOptions.localBuildMode) {
|
||||
return {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (!flags.wait) {
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput(startedBuilds.map(buildInfo => buildInfo.build));
|
||||
}
|
||||
return {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const { accountName } = Object.values(buildCtxByPlatform)[0];
|
||||
@@ -328,7 +294,7 @@ export async function runBuildAndSubmitAsync({
|
||||
[BuildStatus.Errored, BuildStatus.Canceled, BuildStatus.PendingCancel].includes(build?.status)
|
||||
);
|
||||
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags, downloadSimBuildAutoConfirm);
|
||||
await maybeDownloadAndRunSimulatorBuildsAsync(builds, flags);
|
||||
|
||||
if (haveAllBuildsFailedOrCanceled || !flags.autoSubmit) {
|
||||
if (flags.json) {
|
||||
@@ -351,10 +317,6 @@ export async function runBuildAndSubmitAsync({
|
||||
}
|
||||
exitWithNonZeroCodeIfSomeSubmissionsDidntFinish(completedSubmissions);
|
||||
}
|
||||
return {
|
||||
buildIds: startedBuilds.map(({ build }) => build.id),
|
||||
buildProfiles,
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareAndStartBuildAsync({
|
||||
@@ -369,8 +331,6 @@ async function prepareAndStartBuildAsync({
|
||||
vcsClient,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
env,
|
||||
easJsonAccessor,
|
||||
}: {
|
||||
projectDir: string;
|
||||
flags: BuildFlags;
|
||||
@@ -383,8 +343,6 @@ async function prepareAndStartBuildAsync({
|
||||
vcsClient: Client;
|
||||
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
|
||||
customBuildConfigMetadata?: CustomBuildConfigMetadata;
|
||||
easJsonAccessor: EasJsonAccessor;
|
||||
env: Env;
|
||||
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
|
||||
const buildCtx = await createBuildContextAsync({
|
||||
buildProfileName: buildProfile.profileName,
|
||||
@@ -404,22 +362,9 @@ async function prepareAndStartBuildAsync({
|
||||
vcsClient,
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
customBuildConfigMetadata,
|
||||
buildLoggerLevel: flags.buildLoggerLevel ?? (Log.isDebug ? LoggerLevel.DEBUG : undefined),
|
||||
freezeCredentials: flags.freezeCredentials,
|
||||
isVerboseLoggingEnabled: flags.isVerboseLoggingEnabled ?? false,
|
||||
whatToTest: flags.whatToTest,
|
||||
env,
|
||||
buildLoggerLevel: flags.buildLoggerLevel,
|
||||
});
|
||||
|
||||
if (!hasWarnedAboutUsageOverages && !flags.localBuildOptions.localBuildMode) {
|
||||
hasWarnedAboutUsageOverages = true;
|
||||
Log.newLine();
|
||||
await maybeWarnAboutUsageOveragesAsync({ graphqlClient, accountId: buildCtx.accountId });
|
||||
if (!moreBuilds) {
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
if (moreBuilds) {
|
||||
Log.newLine();
|
||||
const appPlatform = toAppPlatform(buildProfile.platform);
|
||||
@@ -430,6 +375,12 @@ async function prepareAndStartBuildAsync({
|
||||
);
|
||||
}
|
||||
|
||||
await validateBuildProfileConfigMatchesProjectConfigAsync(
|
||||
buildCtx.exp,
|
||||
buildProfile,
|
||||
buildCtx.projectId,
|
||||
flags.nonInteractive
|
||||
);
|
||||
if (buildProfile.profile.channel) {
|
||||
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
||||
exp: buildCtx.exp,
|
||||
@@ -439,19 +390,8 @@ async function prepareAndStartBuildAsync({
|
||||
sdkVersion: buildCtx.exp.sdkVersion,
|
||||
nonInteractive: flags.nonInteractive,
|
||||
buildProfile,
|
||||
env: buildProfile.profile.env,
|
||||
easJsonAccessor,
|
||||
});
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
if (
|
||||
isUsingEASUpdate(
|
||||
buildCtx.exp,
|
||||
buildCtx.projectId,
|
||||
easJsonCliConfig.updateManifestHostOverride ?? null
|
||||
)
|
||||
) {
|
||||
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
|
||||
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
|
||||
appId: buildCtx.projectId,
|
||||
channelName: buildProfile.profile.channel,
|
||||
@@ -466,25 +406,6 @@ async function prepareAndStartBuildAsync({
|
||||
}
|
||||
|
||||
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
|
||||
if (
|
||||
easJsonCliConfig?.appVersionSource === undefined &&
|
||||
buildProfile.profile.autoIncrement !== 'version'
|
||||
) {
|
||||
if (buildProfile.profile.autoIncrement !== true) {
|
||||
Log.warn(
|
||||
`The field "cli.appVersionSource" is not set, but it will be required in the future. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/app-versions/'
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
easJsonCliConfig = await ensureAppVersionSourceIsSetAsync(
|
||||
easJsonAccessor,
|
||||
easJsonCliConfig,
|
||||
flags.nonInteractive
|
||||
);
|
||||
}
|
||||
}
|
||||
if (easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE) {
|
||||
validateAppConfigForRemoteVersionSource(buildCtx.exp, buildProfile.platform);
|
||||
}
|
||||
@@ -516,21 +437,21 @@ async function startBuildAsync(ctx: BuildContext<Platform>): Promise<BuildFragme
|
||||
return await sendBuildRequestAsync();
|
||||
}
|
||||
|
||||
async function prepareAndStartAutoSubmissionAsync({
|
||||
async function prepareAndStartSubmissionAsync({
|
||||
build,
|
||||
buildCtx,
|
||||
moreBuilds,
|
||||
projectDir,
|
||||
buildProfile,
|
||||
submitProfile,
|
||||
selectedSubmitProfileName,
|
||||
nonInteractive,
|
||||
}: {
|
||||
build: BuildFragment;
|
||||
buildCtx: BuildContext<Platform>;
|
||||
moreBuilds: boolean;
|
||||
projectDir: string;
|
||||
buildProfile: BuildProfile;
|
||||
submitProfile: SubmitProfile;
|
||||
selectedSubmitProfileName?: string;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<SubmissionFragment> {
|
||||
const platform = toPlatform(build.platform);
|
||||
@@ -540,7 +461,7 @@ async function prepareAndStartAutoSubmissionAsync({
|
||||
profile: submitProfile,
|
||||
archiveFlags: { id: build.id },
|
||||
nonInteractive,
|
||||
env: buildCtx.env,
|
||||
env: buildProfile.env,
|
||||
credentialsCtx: buildCtx.credentialsCtx,
|
||||
applicationIdentifier: buildCtx.android?.applicationId ?? buildCtx.ios?.bundleIdentifier,
|
||||
actor: buildCtx.user,
|
||||
@@ -550,9 +471,6 @@ async function prepareAndStartAutoSubmissionAsync({
|
||||
exp: buildCtx.exp,
|
||||
vcsClient: buildCtx.vcsClient,
|
||||
isVerboseFastlaneEnabled: false,
|
||||
specifiedProfile: selectedSubmitProfileName,
|
||||
groups: undefined, // use groups from submit profile
|
||||
whatToTest: buildCtx.whatToTest,
|
||||
});
|
||||
|
||||
if (moreBuilds) {
|
||||
@@ -576,29 +494,18 @@ function exitWithNonZeroCodeIfSomeBuildsFailed(maybeBuilds: (BuildFragment | nul
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
async function downloadAndRunAsync(build: BuildFragment): Promise<void> {
|
||||
assert(build.artifacts?.applicationArchiveUrl);
|
||||
const cachedAppPath = getEasBuildRunCachedAppPath(build.project.id, build.id, build.platform);
|
||||
|
||||
if (await pathExists(cachedAppPath)) {
|
||||
Log.newLine();
|
||||
Log.log(`Using cached app...`);
|
||||
await runAsync(cachedAppPath, build.platform);
|
||||
return;
|
||||
}
|
||||
|
||||
const buildPath = await downloadAndMaybeExtractAppAsync(
|
||||
build.artifacts.applicationArchiveUrl,
|
||||
build.platform,
|
||||
cachedAppPath
|
||||
build.platform
|
||||
);
|
||||
await runAsync(buildPath, build.platform);
|
||||
}
|
||||
|
||||
async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
builds: MaybeBuildFragment[],
|
||||
flags: BuildFlags,
|
||||
autoConfirm?: boolean
|
||||
flags: BuildFlags
|
||||
): Promise<void> {
|
||||
const simBuilds = builds.filter(truthy).filter(isRunnableOnSimulatorOrEmulator);
|
||||
|
||||
@@ -606,15 +513,11 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
|
||||
for (const simBuild of simBuilds) {
|
||||
if (simBuild.platform === AppPlatform.Android || process.platform === 'darwin') {
|
||||
Log.newLine();
|
||||
const confirm =
|
||||
autoConfirm ??
|
||||
(await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${
|
||||
simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'
|
||||
}?`,
|
||||
}));
|
||||
const confirm = await confirmAsync({
|
||||
message: `Install and run the ${
|
||||
simBuild.platform === AppPlatform.Android ? 'Android' : 'iOS'
|
||||
} build on ${simBuild.platform === AppPlatform.Android ? 'an emulator' : 'a simulator'}?`,
|
||||
});
|
||||
if (confirm) {
|
||||
await downloadAndRunAsync(simBuild);
|
||||
}
|
||||
@@ -631,8 +534,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
||||
buildProfile,
|
||||
nonInteractive,
|
||||
sdkVersion,
|
||||
env,
|
||||
easJsonAccessor,
|
||||
}: {
|
||||
exp: ExpoConfig;
|
||||
projectId: string;
|
||||
@@ -641,8 +542,6 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
||||
buildProfile: ProfileData<'build'>;
|
||||
nonInteractive: boolean;
|
||||
sdkVersion?: string;
|
||||
env: Env | undefined;
|
||||
easJsonAccessor: EasJsonAccessor;
|
||||
}): Promise<void> {
|
||||
if (isExpoUpdatesInstalledOrAvailable(projectDir, sdkVersion)) {
|
||||
return;
|
||||
@@ -664,17 +563,12 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
|
||||
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
|
||||
});
|
||||
if (installExpoUpdates) {
|
||||
const easJsonCliConfig: EasJson['cli'] =
|
||||
(await EasJsonUtils.getCliConfigAsync(easJsonAccessor)) ?? {};
|
||||
|
||||
await ensureEASUpdateIsConfiguredAsync({
|
||||
exp,
|
||||
projectId,
|
||||
projectDir,
|
||||
platform: RequestedPlatform.All,
|
||||
vcsClient,
|
||||
env,
|
||||
manifestHostOverride: easJsonCliConfig.updateManifestHostOverride ?? null,
|
||||
});
|
||||
Log.withTick('Installed expo-updates and configured EAS Update.');
|
||||
throw new Error('Command must be re-run to pick up new updates configuration.');
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { ResourceClass } from '@expo/eas-json';
|
||||
import { LoggerLevel } from '@expo/logger';
|
||||
|
||||
import { LocalBuildOptions } from './local';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
|
||||
export enum BuildStatus {
|
||||
NEW = 'new',
|
||||
IN_QUEUE = 'in-queue',
|
||||
@@ -20,19 +14,3 @@ export enum BuildDistributionType {
|
||||
/** @deprecated Use simulator flag instead */
|
||||
SIMULATOR = 'simulator',
|
||||
}
|
||||
|
||||
export interface BuildFlags {
|
||||
requestedPlatform: RequestedPlatform;
|
||||
profile?: string;
|
||||
nonInteractive: boolean;
|
||||
wait: boolean;
|
||||
clearCache: boolean;
|
||||
json: boolean;
|
||||
autoSubmit: boolean;
|
||||
submitProfile?: string;
|
||||
localBuildOptions: LocalBuildOptions;
|
||||
resourceClass?: ResourceClass;
|
||||
message?: string;
|
||||
buildLoggerLevel?: LoggerLevel;
|
||||
freezeCredentials: boolean;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum DefaultEnvironment {
|
||||
Development = 'development',
|
||||
Preview = 'preview',
|
||||
Production = 'production',
|
||||
}
|
||||
@@ -6,44 +6,8 @@ import {
|
||||
BuildFragment,
|
||||
BuildStatus as GraphQLBuildStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { link } from '../../log';
|
||||
import { appPlatformDisplayNames } from '../../platform';
|
||||
import formatFields, { FormatFieldsItem } from '../../utils/formatFields';
|
||||
|
||||
export function formatGraphQLBuildArtifacts(build: BuildFragment): FormatFieldsItem[] {
|
||||
const fields = [
|
||||
{
|
||||
label: 'Application Archive URL',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
default: {
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? link(url) : 'null';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Build Artifacts URL',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.InProgress:
|
||||
return '<in progress>';
|
||||
default: {
|
||||
const url = build.artifacts?.buildArtifactsUrl;
|
||||
return url ? link(url) : 'null';
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
return fields.filter(({ value }) => value !== undefined && value !== null) as {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
}
|
||||
import formatFields from '../../utils/formatFields';
|
||||
|
||||
export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
const actor = getActorName(build);
|
||||
@@ -75,10 +39,6 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Profile',
|
||||
value: build.buildProfile,
|
||||
},
|
||||
{
|
||||
label: 'Message',
|
||||
value: build.message,
|
||||
@@ -91,6 +51,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
label: 'Enterprise Provisioning',
|
||||
value: build.iosEnterpriseProvisioning?.toLowerCase(),
|
||||
},
|
||||
{
|
||||
label: 'Release Channel',
|
||||
value: build.releaseChannel,
|
||||
},
|
||||
{
|
||||
label: 'Channel',
|
||||
value: build.channel,
|
||||
@@ -117,11 +81,10 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
},
|
||||
{
|
||||
label: 'Logs',
|
||||
value: link(getBuildLogsUrl(build)),
|
||||
value: getBuildLogsUrl(build),
|
||||
},
|
||||
...formatGraphQLBuildArtifacts(build),
|
||||
{
|
||||
label: 'Fingerprint',
|
||||
label: 'Artifact',
|
||||
get value() {
|
||||
switch (build.status) {
|
||||
case GraphQLBuildStatus.New:
|
||||
@@ -133,8 +96,8 @@ export function formatGraphQLBuild(build: BuildFragment): string {
|
||||
case GraphQLBuildStatus.Errored:
|
||||
return null;
|
||||
case GraphQLBuildStatus.Finished: {
|
||||
const hash = build.fingerprint?.hash;
|
||||
return hash ? chalk.green(hash) : chalk.red('not found');
|
||||
const url = build.artifacts?.buildUrl;
|
||||
return url ? url : chalk.red('not found');
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
|
||||
@@ -43,14 +43,14 @@ const errorCodeToErrorMessageOverride: Record<string, (build: BuildFragment) =>
|
||||
|
||||
export function printLogsUrls(builds: BuildFragment[]): void {
|
||||
if (builds.length === 1) {
|
||||
Log.log(`See logs: ${link(getBuildLogsUrl(builds[0]))}`);
|
||||
Log.log(`Build details: ${link(getBuildLogsUrl(builds[0]))}`);
|
||||
} else {
|
||||
builds.forEach(build => {
|
||||
const logsUrl = getBuildLogsUrl(build);
|
||||
Log.log(
|
||||
`See ${appPlatformEmojis[build.platform]} ${
|
||||
`${appPlatformEmojis[build.platform]} ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
} logs: ${link(logsUrl)}`
|
||||
} build details: ${link(logsUrl)}`
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -62,9 +62,7 @@ export function printBuildResults(builds: (BuildFragment | null)[]): void {
|
||||
assert(build, 'Build should be defined');
|
||||
printBuildResult(build);
|
||||
} else {
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => {
|
||||
printBuildResult(build);
|
||||
});
|
||||
(builds.filter(i => i) as BuildFragment[]).forEach(build => printBuildResult(build));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +97,9 @@ function printBuildResult(build: BuildFragment): void {
|
||||
// to the build details page and let people press the button to download there
|
||||
const qrcodeUrl =
|
||||
build.platform === AppPlatform.Ios ? getInternalDistributionInstallUrl(build) : logsUrl;
|
||||
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code => {
|
||||
Log.log(`${indentString(code, 2)}\n`);
|
||||
});
|
||||
qrcodeTerminal.generate(qrcodeUrl, { small: true }, code =>
|
||||
Log.log(`${indentString(code, 2)}\n`)
|
||||
);
|
||||
Log.log(
|
||||
`${appPlatformEmojis[build.platform]} Open this link on your ${
|
||||
appPlatformDisplayNames[build.platform]
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
import tar from 'tar';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import Log, { learnMore } from '../../log';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { formatBytes } from '../../utils/files';
|
||||
@@ -100,22 +100,44 @@ export type LocalFile = {
|
||||
};
|
||||
|
||||
export async function makeProjectMetadataFileAsync(archivePath: string): Promise<LocalFile> {
|
||||
await fs.mkdirp(getTmpDirectory());
|
||||
const spinner = ora('Creating project metadata file');
|
||||
const timerLabel = 'makeProjectMetadataFileAsync';
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
spinner.start();
|
||||
},
|
||||
Log.isDebug ? 1 : 1000
|
||||
);
|
||||
startTimer(timerLabel);
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
try {
|
||||
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,
|
||||
});
|
||||
await fs.writeJSON(metadataLocation, {
|
||||
archiveContent,
|
||||
});
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
if (spinner.isSpinning) {
|
||||
spinner.fail();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
clearTimeout(timer);
|
||||
|
||||
const duration = endTimer(timerLabel);
|
||||
const prettyTime = formatMilliseconds(duration);
|
||||
spinner.succeed(`Created project metadata file ${chalk.dim(prettyTime)}`);
|
||||
|
||||
return { path: metadataLocation, size: await fs.stat(metadataLocation).then(stat => stat.size) };
|
||||
}
|
||||
@@ -169,34 +191,6 @@ export async function makeProjectTarballAsync(vcsClient: Client): Promise<LocalF
|
||||
return { size, path: tarPath };
|
||||
}
|
||||
|
||||
export function maybeWarnAboutProjectTarballSize(size: number): void {
|
||||
if (size <= 150 /* MiB */ * 1024 /* KiB */ * 1024 /* B */) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
`Your project archive is ${formatBytes(
|
||||
size
|
||||
)}. You can reduce its size and the time it takes to upload by excluding files that are unnecessary for the build process in ${chalk.bold(
|
||||
'.easignore'
|
||||
)} file. ${learnMore('https://expo.fyi/eas-build-archive')}`
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_ALLOWED_PROJECT_TARBALL_SIZE =
|
||||
2 /* GiB */ * 1024 /* MiB */ * 1024 /* KiB */ * 1024; /* B */
|
||||
export function assertProjectTarballSizeDoesNotExceedLimit(size: number): void {
|
||||
if (size <= MAX_ALLOWED_PROJECT_TARBALL_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Project archive is too big. Maximum allowed size is ${formatBytes(
|
||||
MAX_ALLOWED_PROJECT_TARBALL_SIZE
|
||||
)}.`
|
||||
);
|
||||
}
|
||||
|
||||
enum ShouldCommitChanges {
|
||||
Yes,
|
||||
ShowDiffFirst,
|
||||
|
||||
@@ -50,34 +50,3 @@ export function getUpdateGroupUrl(
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getWorkflowRunUrl(
|
||||
accountName: string,
|
||||
projectName: string,
|
||||
workflowRunId: string
|
||||
): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/workflows/${workflowRunId}`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getProjectGitHubSettingsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/github`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
|
||||
export function getHostingDeploymentsUrl(accountName: string, projectName: string): string {
|
||||
return new URL(
|
||||
`/accounts/${encodeURIComponent(accountName)}/projects/${encodeURIComponent(
|
||||
projectName
|
||||
)}/hosting/deployments`,
|
||||
getExpoWebsiteBaseUrl()
|
||||
).toString();
|
||||
}
|
||||
@@ -9,9 +9,9 @@ import Log, { learnMore } from '../log';
|
||||
import { isPNGAsync } from '../utils/image';
|
||||
|
||||
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
|
||||
if (ctx.env?.NODE_ENV === 'production') {
|
||||
if (ctx.buildProfile.env?.NODE_ENV === 'production') {
|
||||
Log.warn(
|
||||
'You set NODE_ENV=production in the build profile or environment variables. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
'You set NODE_ENV=production in the build profile. Remember that it will be available during the entire build process. In particular, it will make yarn/npm install only production packages.'
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
@@ -27,28 +27,24 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
|
||||
if (!googleServicesFilePath) {
|
||||
return;
|
||||
}
|
||||
const googleServicesEnvVar =
|
||||
ctx.platform === Platform.ANDROID
|
||||
? ctx.env.GOOGLE_SERVICES_JSON
|
||||
: ctx.env.GOOGLE_SERVICES_INFO_PLIST;
|
||||
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
|
||||
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
|
||||
if (
|
||||
(await fs.pathExists(absGoogleServicesFilePath)) &&
|
||||
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
|
||||
(await ctx.vcsClient.isFileIgnoredAsync(
|
||||
path.relative(rootDir, absGoogleServicesFilePath)
|
||||
))) &&
|
||||
!googleServicesEnvVar
|
||||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
|
||||
) {
|
||||
Log.warn(
|
||||
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
|
||||
);
|
||||
Log.warn(
|
||||
`Use EAS file environment variables with secret or sensitive visibility to pass all values that you don't want to include in your version control to build process. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#file-environment-variables'
|
||||
`Use EAS Secret to pass all values that you don't want to include in your version control. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/variables/#using-secrets-in-environment-variables'
|
||||
)}`
|
||||
);
|
||||
Log.warn(
|
||||
`If you are using that file for compatibility with the classic build service (expo build) you can silence this warning by setting your build profile's env.GOOGLE_SERVICES_FILE in eas.json to any non-empty string.`
|
||||
);
|
||||
Log.newLine();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ import {
|
||||
|
||||
describe(assertVersion, () => {
|
||||
it('throws if the branch mapping is not the correct version', () => {
|
||||
expect(() => {
|
||||
assertVersion(testChannelBasicInfo, 5);
|
||||
}).toThrowError(BranchMappingValidationError);
|
||||
expect(() => assertVersion(testChannelBasicInfo, 5)).toThrowError(BranchMappingValidationError);
|
||||
});
|
||||
it('asserts the correct version', () => {
|
||||
assertVersion(testChannelBasicInfo, 0);
|
||||
|
||||
@@ -18,7 +18,6 @@ export const testUpdateBranch1: UpdateBranchObject = {
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://u.expo.dev/update/bbe3e63a-f620-4603-8c4d-258f3e51590d',
|
||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
||||
isGitWorkingTreeDirty: false,
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
||||
@@ -44,7 +43,6 @@ export const testUpdateBranch1: UpdateBranchObject = {
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://u.expo.dev/update/db22401b-d321-4629-a068-21d6d1f19c02',
|
||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
||||
isGitWorkingTreeDirty: false,
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
||||
@@ -80,7 +78,6 @@ export const testUpdateBranch2: UpdateBranchObject = {
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://u.expo.dev/update/f04aafc1-0bf5-4548-8c35-726b467e7c40',
|
||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
||||
isGitWorkingTreeDirty: false,
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
||||
@@ -106,7 +103,6 @@ export const testUpdateBranch2: UpdateBranchObject = {
|
||||
isRollBackToEmbedded: false,
|
||||
manifestPermalink: 'https://u.expo.dev/update/d8d15786-2727-407f-bf3e-f4bda7e397d9',
|
||||
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
|
||||
isGitWorkingTreeDirty: false,
|
||||
actor: {
|
||||
__typename: 'User',
|
||||
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
|
||||
@@ -132,7 +128,6 @@ export const testChannelObject: UpdateChannelObject = {
|
||||
branchMapping:
|
||||
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
|
||||
updateBranches: [testUpdateBranch1, testUpdateBranch2],
|
||||
isPaused: false,
|
||||
__typename: 'UpdateChannel',
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
*/
|
||||
export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFragment | null> {
|
||||
constructor(
|
||||
private readonly options: {
|
||||
private options: {
|
||||
filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean;
|
||||
printedType?: string;
|
||||
pageSize?: number;
|
||||
@@ -91,8 +91,8 @@ export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFrag
|
||||
queryParams: QueryParams
|
||||
): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
|
||||
filterPredicate
|
||||
? await this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
||||
: await this.queryAsync(ctx, queryParams);
|
||||
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
|
||||
: this.queryAsync(ctx, queryParams);
|
||||
|
||||
const getTitleAsync = async (channelInfo: UpdateChannelBasicInfoFragment): Promise<string> =>
|
||||
channelInfo.name;
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../graphql/client';
|
||||
import {
|
||||
BackgroundJobReceiptDataFragment,
|
||||
ScheduleChannelDeletionMutation,
|
||||
ScheduleChannelDeletionMutationVariables,
|
||||
} from '../graphql/generated';
|
||||
import { BackgroundJobReceiptNode } from '../graphql/types/BackgroundJobReceipt';
|
||||
|
||||
export async function scheduleChannelDeletionAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
channelId,
|
||||
}: {
|
||||
channelId: string;
|
||||
}
|
||||
): Promise<BackgroundJobReceiptDataFragment> {
|
||||
const result = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<ScheduleChannelDeletionMutation, ScheduleChannelDeletionMutationVariables>(
|
||||
gql`
|
||||
mutation ScheduleChannelDeletion($channelId: ID!) {
|
||||
updateChannel {
|
||||
scheduleUpdateChannelDeletion(channelId: $channelId) {
|
||||
id
|
||||
...BackgroundJobReceiptData
|
||||
}
|
||||
}
|
||||
}
|
||||
${BackgroundJobReceiptNode}
|
||||
`,
|
||||
{ channelId }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return result.updateChannel.scheduleUpdateChannelDeletion;
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
|
||||
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
|
||||
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
|
||||
import Log, { learnMore } from '../log';
|
||||
import Log from '../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
|
||||
import formatFields from '../utils/formatFields';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
@@ -89,9 +89,7 @@ export async function listAndRenderChannelsOnAppAsync(
|
||||
queryChannelsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: channels => {
|
||||
renderPageOfChannels(channels, paginatedQueryOptions);
|
||||
},
|
||||
renderListItems: channels => renderPageOfChannels(channels, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -110,11 +108,7 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
|
||||
}
|
||||
): Promise<void> {
|
||||
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, { appId, channelName });
|
||||
renderChannelHeaderContent({
|
||||
channelName: channel.name,
|
||||
channelId: channel.id,
|
||||
isPaused: channel.isPaused,
|
||||
});
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
|
||||
if (paginatedQueryOptions.nonInteractive) {
|
||||
const branches = await queryBranchesAndUpdateGroupsOnChannelAsync(graphqlClient, {
|
||||
@@ -137,9 +131,8 @@ export async function listAndRenderBranchesAndUpdatesOnChannelAsync(
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more channels?',
|
||||
renderListItems: branches => {
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions);
|
||||
},
|
||||
renderListItems: branches =>
|
||||
renderPageOfBranchesOnChannel(channel, branches, paginatedQueryOptions),
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -171,11 +164,7 @@ function renderPageOfChannels(
|
||||
printJsonOnlyOutput({ currentPage });
|
||||
} else {
|
||||
for (const channel of currentPage) {
|
||||
renderChannelHeaderContent({
|
||||
channelName: channel.name,
|
||||
channelId: channel.id,
|
||||
isPaused: channel.isPaused,
|
||||
});
|
||||
renderChannelHeaderContent({ channelName: channel.name, channelId: channel.id });
|
||||
Log.addNewLineIfNone();
|
||||
logChannelDetails(channel);
|
||||
|
||||
@@ -204,11 +193,9 @@ function renderPageOfBranchesOnChannel(
|
||||
function renderChannelHeaderContent({
|
||||
channelName,
|
||||
channelId,
|
||||
isPaused,
|
||||
}: {
|
||||
channelName: string;
|
||||
channelId: string;
|
||||
isPaused: boolean;
|
||||
}): void {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(chalk.bold('Channel:'));
|
||||
@@ -216,7 +203,6 @@ function renderChannelHeaderContent({
|
||||
formatFields([
|
||||
{ label: 'Name', value: channelName },
|
||||
{ label: 'ID', value: channelId },
|
||||
{ label: 'Status', value: isPaused ? 'Paused' : 'Active' },
|
||||
])
|
||||
);
|
||||
Log.addNewLineIfNone();
|
||||
@@ -289,7 +275,7 @@ export async function doesChannelExistAsync(
|
||||
{ appId, channelName }: { appId: string; channelName: string }
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await ChannelQuery.viewUpdateChannelBasicInfoAsync(graphqlClient, {
|
||||
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
|
||||
appId,
|
||||
channelName,
|
||||
});
|
||||
@@ -303,6 +289,7 @@ export async function doesChannelExistAsync(
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Creates a channel and links it to a branch with the same name.
|
||||
*
|
||||
* @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 }
|
||||
): Promise<ChannelBasicInfo> {
|
||||
let branchId: string;
|
||||
let hasCreatedBranch = false;
|
||||
let branchMessage: string;
|
||||
|
||||
try {
|
||||
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
|
||||
@@ -326,6 +313,7 @@ export async function createAndLinkChannelAsync(
|
||||
name: channelName,
|
||||
});
|
||||
branchId = branch.id;
|
||||
branchMessage = `We found a branch with the same name`;
|
||||
} catch (error) {
|
||||
if (error instanceof BranchNotFoundError) {
|
||||
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
|
||||
@@ -333,7 +321,7 @@ export async function createAndLinkChannelAsync(
|
||||
name: channelName,
|
||||
});
|
||||
branchId = newBranch.id;
|
||||
hasCreatedBranch = true;
|
||||
branchMessage = `We also went ahead and made a branch with the same name`;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
@@ -356,25 +344,26 @@ export async function createAndLinkChannelAsync(
|
||||
if (shouldPrintJson) {
|
||||
printJsonOnlyOutput(newChannel);
|
||||
} else {
|
||||
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, appId);
|
||||
const learnMoreLink = learnMore('https://docs.expo.dev/eas-update/eas-cli/');
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
if (hasCreatedBranch) {
|
||||
Log.withTick(
|
||||
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} and branch ${chalk.bold(
|
||||
`"${newChannel.name}"`
|
||||
)} on ${chalk.bold(projectDisplayName)} project. ${learnMoreLink}`
|
||||
);
|
||||
} else {
|
||||
Log.withTick(
|
||||
`Created update channel ${chalk.bold(`"${newChannel.name}"`)} on ${chalk.bold(
|
||||
projectDisplayName
|
||||
)} project and connected it with existing ${chalk.bold(
|
||||
`"${newChannel.name}"`
|
||||
)} branch. ${learnMoreLink}`
|
||||
);
|
||||
}
|
||||
Log.withTick(
|
||||
`Created a new channel on project ${chalk.bold(
|
||||
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
|
||||
)}`
|
||||
);
|
||||
Log.log(
|
||||
formatFields([
|
||||
{ label: 'Name', value: newChannel.name },
|
||||
{ label: 'ID', value: newChannel.id },
|
||||
])
|
||||
);
|
||||
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;
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Command } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
import { GraphQLError } from 'graphql/error';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import AnalyticsContextField from './context/AnalyticsContextField';
|
||||
import ContextField from './context/ContextField';
|
||||
import DynamicLoggedInContextField from './context/DynamicLoggedInContextField';
|
||||
import {
|
||||
DynamicPrivateProjectConfigContextField,
|
||||
DynamicPublicProjectConfigContextField,
|
||||
@@ -16,8 +14,6 @@ import MaybeLoggedInContextField from './context/MaybeLoggedInContextField';
|
||||
import { OptionalPrivateProjectConfigContextField } from './context/OptionalPrivateProjectConfigContextField';
|
||||
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
|
||||
import ProjectDirContextField from './context/ProjectDirContextField';
|
||||
import { ProjectIdContextField } from './context/ProjectIdContextField';
|
||||
import { ServerSideEnvironmentVariablesContextField } from './context/ServerSideEnvironmentVariablesContextField';
|
||||
import SessionManagementContextField from './context/SessionManagementContextField';
|
||||
import VcsClientContextField from './context/VcsClientContextField';
|
||||
import { EasCommandError } from './errors';
|
||||
@@ -26,7 +22,7 @@ import {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import Log, { link } from '../log';
|
||||
import Log from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
@@ -46,26 +42,8 @@ export type ContextOutput<
|
||||
[P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
type GetContextType<Type> = {
|
||||
[Property in keyof Type]: any;
|
||||
};
|
||||
|
||||
const BASE_GRAPHQL_ERROR_MESSAGE: string = 'GraphQL request failed.';
|
||||
|
||||
interface BaseGetContextAsyncArgs {
|
||||
nonInteractive: boolean;
|
||||
vcsClientOverride?: Client;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
|
||||
extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment: string | null;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment?: never;
|
||||
}
|
||||
|
||||
export default abstract class EasCommand extends Command {
|
||||
protected static readonly ContextOptions = {
|
||||
/**
|
||||
@@ -82,16 +60,8 @@ export default abstract class EasCommand extends Command {
|
||||
MaybeLoggedIn: {
|
||||
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.
|
||||
* @deprecated Should not be used outside of session management commands, which currently only includes `login` and `logout`.
|
||||
*/
|
||||
SessionManagment: {
|
||||
sessionManager: new SessionManagementContextField(),
|
||||
@@ -101,7 +71,7 @@ export default abstract class EasCommand extends Command {
|
||||
* run within a project directory, null otherwise.
|
||||
*/
|
||||
OptionalProjectConfig: {
|
||||
optionalPrivateProjectConfig: new OptionalPrivateProjectConfigContextField(),
|
||||
privateProjectConfig: new OptionalPrivateProjectConfigContextField(),
|
||||
},
|
||||
/**
|
||||
* Require this command to be run in a project directory. Return the project directory in the context.
|
||||
@@ -136,16 +106,6 @@ export default abstract class EasCommand extends Command {
|
||||
Vcs: {
|
||||
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(),
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -180,19 +140,7 @@ export default abstract class EasCommand extends Command {
|
||||
} = object,
|
||||
>(
|
||||
commandClass: { contextDefinition: ContextInput<C> },
|
||||
{
|
||||
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
|
||||
{ nonInteractive, vcsClientOverride }: { nonInteractive: boolean; vcsClientOverride?: Client }
|
||||
): Promise<ContextOutput<C>> {
|
||||
const contextDefinition = commandClass.contextDefinition;
|
||||
|
||||
@@ -206,7 +154,6 @@ export default abstract class EasCommand extends Command {
|
||||
sessionManager: this.sessionManager,
|
||||
analytics: this.analytics,
|
||||
vcsClientOverride,
|
||||
withServerSideEnvironment,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
@@ -239,13 +186,13 @@ export default abstract class EasCommand extends Command {
|
||||
action: `eas ${this.id}`,
|
||||
});
|
||||
|
||||
return await this.runAsync();
|
||||
return this.runAsync();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line async-protect/async-suffix
|
||||
override async finally(err: Error): Promise<any> {
|
||||
await this.analytics.flushAsync();
|
||||
return await super.finally(err);
|
||||
return super.finally(err);
|
||||
}
|
||||
|
||||
protected override catch(err: Error): Promise<any> {
|
||||
@@ -253,7 +200,7 @@ export default abstract class EasCommand extends Command {
|
||||
if (err instanceof EasCommandError) {
|
||||
Log.error(err.message);
|
||||
} else if (err instanceof CombinedError && err?.graphQLErrors) {
|
||||
const cleanGQLErrorsMessage = err?.graphQLErrors
|
||||
const cleanMessage = err?.graphQLErrors
|
||||
.map((graphQLError: GraphQLError) => {
|
||||
const messageLine = graphQLError.message.replace('[GraphQL] ', '');
|
||||
const requestIdLine = graphQLError.extensions?.requestId
|
||||
@@ -263,31 +210,12 @@ export default abstract class EasCommand extends Command {
|
||||
const defaultMsg = `${messageLine}${requestIdLine}`;
|
||||
|
||||
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
|
||||
if (defaultMsg.includes('ScopedAccountActorViewerContext') && process.env.EAS_BUILD) {
|
||||
// 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 `You don't have the required permissions to perform this operation.\n\n${defaultMsg}`;
|
||||
}
|
||||
|
||||
return defaultMsg;
|
||||
})
|
||||
.join('\n');
|
||||
const cleanMessage = err.networkError
|
||||
? `${cleanGQLErrorsMessage}\n${err.networkError.message}`
|
||||
: cleanGQLErrorsMessage;
|
||||
Log.error(cleanMessage);
|
||||
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
|
||||
} else {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
experienceToAccountName,
|
||||
fullNameToSlug,
|
||||
isValidExperienceName,
|
||||
isValidSlugStrict,
|
||||
} from '../experienceParser';
|
||||
|
||||
describe(isValidExperienceName, () => {
|
||||
it.each([
|
||||
['wat', false],
|
||||
['@wat', false],
|
||||
['@wat/', false],
|
||||
['@wat/hello', true],
|
||||
])('case %p', (candidate, shouldBeValid) => {
|
||||
expect(isValidExperienceName(candidate)).toEqual(shouldBeValid);
|
||||
});
|
||||
});
|
||||
|
||||
describe(experienceToAccountName, () => {
|
||||
it('parses account name', () => {
|
||||
expect(experienceToAccountName('@wat/huh')).toEqual('wat');
|
||||
});
|
||||
});
|
||||
|
||||
describe(fullNameToSlug, () => {
|
||||
it('parses slug', () => {
|
||||
expect(fullNameToSlug('@wat/huh')).toEqual('huh');
|
||||
});
|
||||
});
|
||||
|
||||
describe(isValidSlugStrict, () => {
|
||||
it.each([
|
||||
['wat', true],
|
||||
['wat123456-hello-what123456', true],
|
||||
['@wat', false],
|
||||
['@wat/', false],
|
||||
['@wat/hello', false],
|
||||
])('case %p', (candidate, shouldBeValid) => {
|
||||
expect(isValidSlugStrict(candidate)).toEqual(shouldBeValid);
|
||||
});
|
||||
});
|
||||
@@ -38,13 +38,7 @@ export async function fetchBuildsAsync({
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
filters?: {
|
||||
statuses?: BuildStatus[];
|
||||
platform?: RequestedPlatform;
|
||||
profile?: string;
|
||||
hasFingerprint?: boolean;
|
||||
fingerprintHash?: string;
|
||||
};
|
||||
filters?: { statuses?: BuildStatus[]; platform?: RequestedPlatform; profile?: string };
|
||||
}): Promise<BuildFragment[]> {
|
||||
let builds: BuildFragment[];
|
||||
const queryFilters: InputMaybe<BuildFilter> = {};
|
||||
@@ -54,12 +48,6 @@ export async function fetchBuildsAsync({
|
||||
if (filters?.profile) {
|
||||
queryFilters['buildProfile'] = filters.profile;
|
||||
}
|
||||
if (filters?.hasFingerprint) {
|
||||
queryFilters['hasFingerprint'] = filters.hasFingerprint;
|
||||
}
|
||||
if (filters?.fingerprintHash) {
|
||||
queryFilters['fingerprintHash'] = filters.fingerprintHash;
|
||||
}
|
||||
if (!filters?.statuses) {
|
||||
builds = await BuildQuery.viewBuildsOnAppAsync(graphqlClient, {
|
||||
appId: projectId,
|
||||
|
||||
@@ -7,10 +7,6 @@ export interface ContextOptions {
|
||||
analytics: Analytics;
|
||||
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?: string | null;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
Loaded 100 of 648 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user