mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c31702653 | ||
|
|
b053e0926a | ||
|
|
6eeb15bdce | ||
|
|
279eb1595c | ||
|
|
256b02d3e9 | ||
|
|
9f4fef7626 | ||
|
|
8cc324e11e | ||
|
|
f33b4c131d | ||
|
|
e3eb3ad5ea | ||
|
|
1292465c57 | ||
|
|
00d2022680 | ||
|
|
5aaaf1b224 | ||
|
|
aaf14b8bab | ||
|
|
65413d1640 | ||
|
|
71ee9469aa | ||
|
|
7223a19fc8 | ||
|
|
3f4e826315 | ||
|
|
ea7cc6b1ea | ||
|
|
b5281d64d1 | ||
|
|
e51fe11aca | ||
|
|
16767f3feb | ||
|
|
c42b1a004c | ||
|
|
8a4bf6f068 | ||
|
|
03e1c18d34 | ||
|
|
2bf8822b1d | ||
|
|
460f98da76 | ||
|
|
3a7182bb82 | ||
|
|
f963dc711b | ||
|
|
a059852bd9 | ||
|
|
33e0b0901c | ||
|
|
8ba877ab81 | ||
|
|
9f6e387888 | ||
|
|
c10691c339 | ||
|
|
155b65c8fc | ||
|
|
cdbc95295c | ||
|
|
414ccb92c0 | ||
|
|
aabf519a9d | ||
|
|
88d9627286 | ||
|
|
59ce6d0b65 | ||
|
|
8e64a9c73e | ||
|
|
4476c87962 | ||
|
|
18ecd0844a | ||
|
|
cc3e7cdbe4 | ||
|
|
aa7fecb9fe | ||
|
|
c73a3e3a1e | ||
|
|
37ca2ebd55 | ||
|
|
dead11964e | ||
|
|
b66d31d7d7 | ||
|
|
ed8170047b | ||
|
|
0e4056ba82 | ||
|
|
6182ea5a1d | ||
|
|
c9e7b649ad | ||
|
|
7a6b796242 | ||
|
|
804eb9a326 | ||
|
|
8da14f0101 | ||
|
|
dc62d22816 | ||
|
|
258c625611 | ||
|
|
e84340774d | ||
|
|
fbc23f7a7d | ||
|
|
97255ec90d | ||
|
|
cfccc87b82 | ||
|
|
09b4f8367e | ||
|
|
45481f9758 | ||
|
|
1119008317 | ||
|
|
123a0c1b7e | ||
|
|
2f21fa62b1 | ||
|
|
8ad449c6b7 | ||
|
|
fe8ec50806 | ||
|
|
86cefeb259 | ||
|
|
034e0a1e7f | ||
|
|
a6b776915f | ||
|
|
4ae52d0e3d | ||
|
|
f62720ef5a | ||
|
|
af09aaedb3 |
No files matched your search
@@ -0,0 +1,17 @@
|
||||
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"
|
||||
@@ -0,0 +1,16 @@
|
||||
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
|
||||
@@ -0,0 +1,205 @@
|
||||
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,64 @@
|
||||
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,6 +9,8 @@ jobs:
|
||||
release:
|
||||
name: Create a release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
@@ -24,9 +26,11 @@ jobs:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -69,9 +73,11 @@ jobs:
|
||||
# Use macOS to get the correct platform specific dependencies
|
||||
runs-on: macos-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: brew install python-setuptools
|
||||
@@ -112,9 +118,11 @@ jobs:
|
||||
name: Build for Windows
|
||||
runs-on: ubuntu-latest
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: yarn install --frozen-lockfile --check-files
|
||||
@@ -152,9 +160,11 @@ jobs:
|
||||
name: Publish to npm
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-linux, build-mac, build-windows]
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org/'
|
||||
scope: 'expo'
|
||||
@@ -165,31 +175,25 @@ jobs:
|
||||
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@v2
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.EXPO_BOT_PAT }}
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- name: Install dependencies
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['20', '22']
|
||||
node: ['20', '22', '24']
|
||||
include:
|
||||
- node: '20'
|
||||
coverage: true
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
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 }}
|
||||
+89
-2
@@ -8,14 +8,101 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Add `--no-bytecode` and `--source-maps [mode]` flags to `eas update`. ([#3339](https://github.com/expo/eas-cli/pull/3339) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Add App Clip bundle identifier registration support for multi-target iOS builds. ([#3300](https://github.com/expo/eas-cli/pull/3300) by [@evanbacon](https://github.com/evanbacon))
|
||||
- Add `--runtime-version` and `--platform` filters to `eas update:list`. ([#3261](https://github.com/expo/eas-cli/pull/3261) by [@HarelSultan](https://github.com/HarelSultan))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `--source-maps` flag compatibility with older SDKs that only support it as a boolean flag. ([#3341](https://github.com/expo/eas-cli/pull/3341) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Use `--dump-sourcemaps` as fallback when `--source-maps` is not provided to `eas update`, for backwards compatibility. ([8cc324e1](https://github.com/expo/eas-cli/commit/8cc324e1) by [@brentvatne](https://github.com/brentvatne))
|
||||
- Fix `metadata:pull` failing for apps with only a live version by falling back to live app version and info. ([#3299](https://github.com/expo/eas-cli/pull/3299) by [@EvanBacon](https://github.com/EvanBacon))
|
||||
- eas init should fix and validate project name and slug. ([#3277](https://github.com/expo/eas-cli/pull/3277) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Test with node 24. ([#3270](https://github.com/expo/eas-cli/pull/3270) by [@wschurman](https://github.com/wschurman))
|
||||
- Delete channel in background. ([#3278](https://github.com/expo/eas-cli/pull/3278) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Add warning to channel:delete. ([#3335](https://github.com/expo/eas-cli/pull/3335) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.28.0](https://github.com/expo/eas-cli/releases/tag/v16.28.0) - 2025-11-20
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas:new` sorts organization accounts first in account selection. ([#3256](https://github.com/expo/eas-cli/pull/3256) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix `eas:build` bug where overages are checked before config is ready. ([#3260](https://github.com/expo/eas-cli/pull/3260) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Delete branch in background. ([#3258](https://github.com/expo/eas-cli/pull/3258) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
## [16.27.0](https://github.com/expo/eas-cli/releases/tag/v16.27.0) - 2025-11-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Kick off update patch generation if enabled - this is feature is in preview, don't use it for prod yet please. ([#3250](https://github.com/expo/eas-cli/pull/3250) by [@quinlanj](https://github.com/quinlanj))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Fix workflow:status when run ID passed in. ([#3253](https://github.com/expo/eas-cli/pull/3253) by [@douglowder](https://github.com/douglowder))
|
||||
- Fix updating file env vars with `env:update --non-interactive`. ([#3249](https://github.com/expo/eas-cli/pull/3249) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Fix yarn watch. ([#3251](https://github.com/expo/eas-cli/pull/3251) by [@quinlanj](https://github.com/quinlanj))
|
||||
- Remove the **node_modules** installation requirement for `workflow:run` command. ([#3254](https://github.com/expo/eas-cli/pull/3254) by [@kudo](https://github.com/kudo))
|
||||
|
||||
## [16.26.0](https://github.com/expo/eas-cli/releases/tag/v16.26.0) - 2025-10-29
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- New command `workflow:status`. ([#3242](https://github.com/expo/eas-cli/pull/3242) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- `eas:new` cleanup (remove `hidden` tag, fix README error, output cleanup). ([#3243](https://github.com/expo/eas-cli/pull/3243) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.25.1](https://github.com/expo/eas-cli/releases/tag/v16.25.1) - 2025-10-28
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Remove preview label from EAS Hosting commands ([#3241](https://github.com/expo/eas-cli/pull/3241) by [@kitten](https://github.com/kitten))
|
||||
|
||||
## [16.25.0](https://github.com/expo/eas-cli/releases/tag/v16.25.0) - 2025-10-27
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- `eas:new` command changes (project name prompt, output cleanup, additional project files, additional prompts). ([#3235](https://github.com/expo/eas-cli/pull/3235) by [@mackenco](https://github.com/mackenco))
|
||||
- `eas:build` command displays a warning when account is near it's build limit. ([#3236](https://github.com/expo/eas-cli/pull/3236) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:run can use --wait and --json together. ([#3239](https://github.com/expo/eas-cli/pull/3239) by [@douglowder](https://github.com/douglowder))
|
||||
- Add missing env for local builds with latest plugin. ([#3237](https://github.com/expo/eas-cli/pull/3237) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- Document the default values for `track` and `releaseStatus`. ([#3229](https://github.com/expo/eas-cli/pull/3229) by [@kadikraman](https://github.com/kadikraman))
|
||||
|
||||
## [16.24.1](https://github.com/expo/eas-cli/releases/tag/v16.24.1) - 2025-10-19
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- Revert change in ora version. ([#3231](https://github.com/expo/eas-cli/pull/3231) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.24.0](https://github.com/expo/eas-cli/releases/tag/v16.24.0) - 2025-10-19
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- workflow:run --wait shows periodic job and step status updates. ([#3227](https://github.com/expo/eas-cli/pull/3227) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- workflow:logs should show logs from build and repack jobs. ([#3226](https://github.com/expo/eas-cli/pull/3226) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [16.23.1](https://github.com/expo/eas-cli/releases/tag/v16.23.1) - 2025-10-16
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# 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
|
||||
+3
-1
@@ -4,9 +4,11 @@ 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],
|
||||
projects: [easCliConfig, easJsonConfig, workerConfig],
|
||||
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.24.0",
|
||||
"version": "16.30.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+4
-4
@@ -21,21 +21,21 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/node": "20.11.0",
|
||||
"@types/node": "20.14.8",
|
||||
"eslint": "8.56.0",
|
||||
"eslint-config-universe": "13.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": "8.2.3",
|
||||
"lerna": "9.0.3",
|
||||
"oclif": "^3.9.0",
|
||||
"prettier": "3.1.1",
|
||||
"ts-jest": "29.1.1",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"node": "20.19.4",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-build-cache-provider",
|
||||
"description": "A build cache provider plugin for the Expo CLI",
|
||||
"version": "16.4.2",
|
||||
"version": "16.30.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"@types/getenv": "^1.0.0",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -33,7 +33,11 @@
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"repository": "expo/eas-cli",
|
||||
"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",
|
||||
@@ -50,9 +54,5 @@
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
# 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
|
||||
+172
-94
@@ -120,10 +120,12 @@ eas --help COMMAND
|
||||
* [`eas metadata:lint`](#eas-metadatalint)
|
||||
* [`eas metadata:pull`](#eas-metadatapull)
|
||||
* [`eas metadata:push`](#eas-metadatapush)
|
||||
* [`eas new [PATH]`](#eas-new-path)
|
||||
* [`eas onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-onboarding-target_project_directory)
|
||||
* [`eas open`](#eas-open)
|
||||
* [`eas project:info`](#eas-projectinfo)
|
||||
* [`eas project:init`](#eas-projectinit)
|
||||
* [`eas project:new [PATH]`](#eas-projectnew-path)
|
||||
* [`eas project:onboarding [TARGET_PROJECT_DIRECTORY]`](#eas-projectonboarding-target_project_directory)
|
||||
* [`eas submit`](#eas-submit)
|
||||
* [`eas update`](#eas-update)
|
||||
@@ -152,6 +154,7 @@ eas --help COMMAND
|
||||
* [`eas workflow:logs [ID]`](#eas-workflowlogs-id)
|
||||
* [`eas workflow:run [FILE]`](#eas-workflowrun-file)
|
||||
* [`eas workflow:runs`](#eas-workflowruns)
|
||||
* [`eas workflow:status [WORKFLOW_RUN_ID]`](#eas-workflowstatus-workflow_run_id)
|
||||
* [`eas workflow:validate PATH`](#eas-workflowvalidate-path)
|
||||
* [`eas workflow:view [ID]`](#eas-workflowview-id)
|
||||
|
||||
@@ -173,7 +176,7 @@ ALIASES
|
||||
$ eas login
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/account/login.ts)_
|
||||
|
||||
## `eas account:logout`
|
||||
|
||||
@@ -190,7 +193,7 @@ ALIASES
|
||||
$ eas logout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/account/logout.ts)_
|
||||
|
||||
## `eas account:view`
|
||||
|
||||
@@ -207,7 +210,7 @@ ALIASES
|
||||
$ eas whoami
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/account/view.ts)_
|
||||
|
||||
## `eas analytics [STATUS]`
|
||||
|
||||
@@ -221,7 +224,7 @@ DESCRIPTION
|
||||
display or change analytics settings
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/analytics.ts)_
|
||||
|
||||
## `eas autocomplete [SHELL]`
|
||||
|
||||
@@ -273,7 +276,7 @@ DESCRIPTION
|
||||
create a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/branch/create.ts)_
|
||||
|
||||
## `eas branch:delete [NAME]`
|
||||
|
||||
@@ -294,7 +297,7 @@ DESCRIPTION
|
||||
delete a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/branch/delete.ts)_
|
||||
|
||||
## `eas branch:list`
|
||||
|
||||
@@ -314,7 +317,7 @@ DESCRIPTION
|
||||
list all branches
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/branch/list.ts)_
|
||||
|
||||
## `eas branch:rename`
|
||||
|
||||
@@ -334,7 +337,7 @@ DESCRIPTION
|
||||
rename a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/branch/rename.ts)_
|
||||
|
||||
## `eas branch:view [NAME]`
|
||||
|
||||
@@ -357,7 +360,7 @@ DESCRIPTION
|
||||
view a branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/branch/view.ts)_
|
||||
|
||||
## `eas build`
|
||||
|
||||
@@ -398,7 +401,7 @@ DESCRIPTION
|
||||
start a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/index.ts)_
|
||||
|
||||
## `eas build:cancel [BUILD_ID]`
|
||||
|
||||
@@ -417,7 +420,7 @@ DESCRIPTION
|
||||
cancel a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/cancel.ts)_
|
||||
|
||||
## `eas build:configure`
|
||||
|
||||
@@ -434,7 +437,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/configure.ts)_
|
||||
|
||||
## `eas build:delete [BUILD_ID]`
|
||||
|
||||
@@ -453,7 +456,7 @@ DESCRIPTION
|
||||
delete a build
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/delete.ts)_
|
||||
|
||||
## `eas build:dev`
|
||||
|
||||
@@ -473,7 +476,7 @@ DESCRIPTION
|
||||
run dev client simulator/emulator build with matching fingerprint or create a new one
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/dev.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/dev.ts)_
|
||||
|
||||
## `eas build:download`
|
||||
|
||||
@@ -494,7 +497,7 @@ DESCRIPTION
|
||||
download simulator/emulator builds for a given fingerprint hash
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/download.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/download.ts)_
|
||||
|
||||
## `eas build:inspect`
|
||||
|
||||
@@ -529,7 +532,7 @@ DESCRIPTION
|
||||
inspect the state of the project at specific build stages, useful for troubleshooting
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/inspect.ts)_
|
||||
|
||||
## `eas build:list`
|
||||
|
||||
@@ -581,7 +584,7 @@ DESCRIPTION
|
||||
list all builds for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/list.ts)_
|
||||
|
||||
## `eas build:resign`
|
||||
|
||||
@@ -610,7 +613,7 @@ DESCRIPTION
|
||||
re-sign a build archive
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/resign.ts)_
|
||||
|
||||
## `eas build:run`
|
||||
|
||||
@@ -636,7 +639,7 @@ DESCRIPTION
|
||||
run simulator/emulator builds from eas-cli
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/run.ts)_
|
||||
|
||||
## `eas build:submit`
|
||||
|
||||
@@ -690,7 +693,7 @@ DESCRIPTION
|
||||
get the latest version from EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/version/get.ts)_
|
||||
|
||||
## `eas build:version:set`
|
||||
|
||||
@@ -709,7 +712,7 @@ DESCRIPTION
|
||||
update version of an app
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/version/set.ts)_
|
||||
|
||||
## `eas build:version:sync`
|
||||
|
||||
@@ -728,7 +731,7 @@ DESCRIPTION
|
||||
update a version in native code with a value stored on EAS servers
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/version/sync.ts)_
|
||||
|
||||
## `eas build:view [BUILD_ID]`
|
||||
|
||||
@@ -745,7 +748,7 @@ DESCRIPTION
|
||||
view a build for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/build/view.ts)_
|
||||
|
||||
## `eas channel:create [NAME]`
|
||||
|
||||
@@ -766,7 +769,7 @@ DESCRIPTION
|
||||
create a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/create.ts)_
|
||||
|
||||
## `eas channel:delete [NAME]`
|
||||
|
||||
@@ -787,7 +790,7 @@ DESCRIPTION
|
||||
Delete a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/delete.ts)_
|
||||
|
||||
## `eas channel:edit [NAME]`
|
||||
|
||||
@@ -809,7 +812,7 @@ DESCRIPTION
|
||||
point a channel at a new branch
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/edit.ts)_
|
||||
|
||||
## `eas channel:list`
|
||||
|
||||
@@ -829,7 +832,7 @@ DESCRIPTION
|
||||
list all channels
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/list.ts)_
|
||||
|
||||
## `eas channel:pause [NAME]`
|
||||
|
||||
@@ -851,7 +854,7 @@ DESCRIPTION
|
||||
pause a channel to stop it from sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/pause.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/pause.ts)_
|
||||
|
||||
## `eas channel:resume [NAME]`
|
||||
|
||||
@@ -873,7 +876,7 @@ DESCRIPTION
|
||||
resume a channel to start sending updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/resume.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/resume.ts)_
|
||||
|
||||
## `eas channel:rollout [CHANNEL]`
|
||||
|
||||
@@ -906,7 +909,7 @@ DESCRIPTION
|
||||
Roll a new branch out on a channel incrementally.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/rollout.ts)_
|
||||
|
||||
## `eas channel:view [NAME]`
|
||||
|
||||
@@ -929,7 +932,7 @@ DESCRIPTION
|
||||
view a channel
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/channel/view.ts)_
|
||||
|
||||
## `eas config`
|
||||
|
||||
@@ -950,7 +953,7 @@ DESCRIPTION
|
||||
display project configuration (app.json + eas.json)
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/config.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/config.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/config.ts)_
|
||||
|
||||
## `eas credentials`
|
||||
|
||||
@@ -967,7 +970,7 @@ DESCRIPTION
|
||||
manage credentials
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/index.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/credentials/index.ts)_
|
||||
|
||||
## `eas credentials:configure-build`
|
||||
|
||||
@@ -985,7 +988,7 @@ DESCRIPTION
|
||||
Set up credentials for building your project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/credentials/configure-build.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/credentials/configure-build.ts)_
|
||||
|
||||
## `eas deploy [options]`
|
||||
|
||||
@@ -1013,7 +1016,7 @@ ALIASES
|
||||
$ eas worker:deploy
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/index.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/deploy/index.ts)_
|
||||
|
||||
## `eas deploy:alias`
|
||||
|
||||
@@ -1038,7 +1041,7 @@ ALIASES
|
||||
$ eas deploy:promote
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/index.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/deploy/alias/index.ts)_
|
||||
|
||||
## `eas deploy:alias:delete [ALIAS_NAME]`
|
||||
|
||||
@@ -1059,7 +1062,7 @@ ALIASES
|
||||
$ eas worker:alias:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/alias/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/deploy/alias/delete.ts)_
|
||||
|
||||
## `eas deploy:delete [DEPLOYMENT_ID]`
|
||||
|
||||
@@ -1080,7 +1083,7 @@ ALIASES
|
||||
$ eas worker:delete
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/deploy/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/deploy/delete.ts)_
|
||||
|
||||
## `eas deploy:promote`
|
||||
|
||||
@@ -1117,7 +1120,7 @@ DESCRIPTION
|
||||
register new Apple Devices to use for internal distribution
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/device/create.ts)_
|
||||
|
||||
## `eas device:delete`
|
||||
|
||||
@@ -1137,7 +1140,7 @@ DESCRIPTION
|
||||
remove a registered device from your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/device/delete.ts)_
|
||||
|
||||
## `eas device:list`
|
||||
|
||||
@@ -1158,7 +1161,7 @@ DESCRIPTION
|
||||
list all registered devices for your account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/device/list.ts)_
|
||||
|
||||
## `eas device:rename`
|
||||
|
||||
@@ -1179,7 +1182,7 @@ DESCRIPTION
|
||||
rename a registered device
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/device/rename.ts)_
|
||||
|
||||
## `eas device:view [UDID]`
|
||||
|
||||
@@ -1193,7 +1196,7 @@ DESCRIPTION
|
||||
view a device for your project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/device/view.ts)_
|
||||
|
||||
## `eas diagnostics`
|
||||
|
||||
@@ -1207,7 +1210,7 @@ DESCRIPTION
|
||||
display environment info
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/diagnostics.ts)_
|
||||
|
||||
## `eas env:create [ENVIRONMENT]`
|
||||
|
||||
@@ -1237,7 +1240,7 @@ DESCRIPTION
|
||||
create an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/create.ts)_
|
||||
|
||||
## `eas env:delete [ENVIRONMENT]`
|
||||
|
||||
@@ -1262,7 +1265,7 @@ DESCRIPTION
|
||||
delete an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/delete.ts)_
|
||||
|
||||
## `eas env:exec ENVIRONMENT BASH_COMMAND`
|
||||
|
||||
@@ -1284,7 +1287,7 @@ DESCRIPTION
|
||||
execute a command with environment variables from the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/exec.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/exec.ts)_
|
||||
|
||||
## `eas env:get [ENVIRONMENT]`
|
||||
|
||||
@@ -1309,7 +1312,7 @@ DESCRIPTION
|
||||
view an environment variable for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/get.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/get.ts)_
|
||||
|
||||
## `eas env:list [ENVIRONMENT]`
|
||||
|
||||
@@ -1335,7 +1338,7 @@ DESCRIPTION
|
||||
list environment variables for the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/list.ts)_
|
||||
|
||||
## `eas env:pull [ENVIRONMENT]`
|
||||
|
||||
@@ -1357,7 +1360,7 @@ DESCRIPTION
|
||||
pull environment variables for the selected environment to .env file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/pull.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/pull.ts)_
|
||||
|
||||
## `eas env:push [ENVIRONMENT]`
|
||||
|
||||
@@ -1379,7 +1382,7 @@ DESCRIPTION
|
||||
push environment variables from .env file to the selected environment
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/push.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/push.ts)_
|
||||
|
||||
## `eas env:update [ENVIRONMENT]`
|
||||
|
||||
@@ -1411,7 +1414,7 @@ DESCRIPTION
|
||||
update an environment variable on the current project or account
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/env/update.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/env/update.ts)_
|
||||
|
||||
## `eas fingerprint:compare [HASH1] [HASH2]`
|
||||
|
||||
@@ -1455,7 +1458,7 @@ EXAMPLES
|
||||
$ eas fingerprint:compare <FINGERPRINT-HASH> --update-id <UPDATE-ID> # Compare fingerprint from update against provided fingerprint
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/compare.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/fingerprint/compare.ts)_
|
||||
|
||||
## `eas fingerprint:generate`
|
||||
|
||||
@@ -1485,7 +1488,7 @@ EXAMPLES
|
||||
$ eas fingerprint:generate --json --non-interactive --platform android # Output fingerprint json to stdout
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/fingerprint/generate.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/fingerprint/generate.ts)_
|
||||
|
||||
## `eas help [COMMAND]`
|
||||
|
||||
@@ -1593,7 +1596,7 @@ DESCRIPTION
|
||||
validate the local store configuration
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/metadata/lint.ts)_
|
||||
|
||||
## `eas metadata:pull`
|
||||
|
||||
@@ -1610,7 +1613,7 @@ DESCRIPTION
|
||||
generate the local store configuration from the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/metadata/pull.ts)_
|
||||
|
||||
## `eas metadata:push`
|
||||
|
||||
@@ -1627,7 +1630,28 @@ DESCRIPTION
|
||||
sync the local store configuration to the app stores
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/metadata/push.ts)_
|
||||
|
||||
## `eas new [PATH]`
|
||||
|
||||
Create a new project configured with Expo Application Services (EAS)
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas new [PATH] [-p bun|npm|pnpm|yarn]
|
||||
|
||||
ARGUMENTS
|
||||
PATH Path to create the project (defaults to current directory)
|
||||
|
||||
FLAGS
|
||||
-p, --package-manager=(bun|npm|pnpm|yarn) [default: npm] Package manager to use for installing dependencies
|
||||
|
||||
DESCRIPTION
|
||||
Create a new project configured with Expo Application Services (EAS)
|
||||
|
||||
ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
## `eas onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1657,7 +1681,7 @@ DESCRIPTION
|
||||
open the project page in a web browser
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/open.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/open.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/open.ts)_
|
||||
|
||||
## `eas project:info`
|
||||
|
||||
@@ -1671,7 +1695,7 @@ DESCRIPTION
|
||||
information about the current project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/project/info.ts)_
|
||||
|
||||
## `eas project:init`
|
||||
|
||||
@@ -1694,7 +1718,30 @@ ALIASES
|
||||
$ eas init
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/project/init.ts)_
|
||||
|
||||
## `eas project:new [PATH]`
|
||||
|
||||
Create a new project configured with Expo Application Services (EAS)
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas project:new [PATH] [-p bun|npm|pnpm|yarn]
|
||||
|
||||
ARGUMENTS
|
||||
PATH Path to create the project (defaults to current directory)
|
||||
|
||||
FLAGS
|
||||
-p, --package-manager=(bun|npm|pnpm|yarn) [default: npm] Package manager to use for installing dependencies
|
||||
|
||||
DESCRIPTION
|
||||
Create a new project configured with Expo Application Services (EAS)
|
||||
|
||||
ALIASES
|
||||
$ eas new
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/new.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/project/new.ts)_
|
||||
|
||||
## `eas project:onboarding [TARGET_PROJECT_DIRECTORY]`
|
||||
|
||||
@@ -1712,7 +1759,7 @@ ALIASES
|
||||
$ eas onboarding
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/project/onboarding.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/project/onboarding.ts)_
|
||||
|
||||
## `eas submit`
|
||||
|
||||
@@ -1747,7 +1794,7 @@ ALIASES
|
||||
$ eas build:submit
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/submit.ts)_
|
||||
|
||||
## `eas update`
|
||||
|
||||
@@ -1787,7 +1834,7 @@ DESCRIPTION
|
||||
publish an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/index.ts)_
|
||||
|
||||
## `eas update:configure`
|
||||
|
||||
@@ -1807,7 +1854,7 @@ DESCRIPTION
|
||||
configure the project to support EAS Update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/configure.ts)_
|
||||
|
||||
## `eas update:delete GROUPID`
|
||||
|
||||
@@ -1828,7 +1875,7 @@ DESCRIPTION
|
||||
delete all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/delete.ts)_
|
||||
|
||||
## `eas update:edit [GROUPID]`
|
||||
|
||||
@@ -1852,7 +1899,7 @@ DESCRIPTION
|
||||
edit all the updates in an update group
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/edit.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/edit.ts)_
|
||||
|
||||
## `eas update:list`
|
||||
|
||||
@@ -1860,21 +1907,24 @@ view the recent updates
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas update:list [--branch <value> | --all] [--offset <value>] [--limit <value>] [--json --non-interactive]
|
||||
$ eas update:list [--branch <value> | --all] [-p android|ios|all] [--runtime-version <value>] [--offset
|
||||
<value>] [--limit <value>] [--json --non-interactive]
|
||||
|
||||
FLAGS
|
||||
--all List updates on all branches
|
||||
--branch=<value> List updates only on this branch
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 25 and is capped at 50.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
-p, --platform=(android|ios|all) Filter updates by platform
|
||||
--all List updates on all branches
|
||||
--branch=<value> List updates only on this branch
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--limit=<value> The number of items to fetch each query. Defaults to 25 and is capped at 50.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--offset=<value> Start queries from specified index. Use for paginating results. Defaults to 0.
|
||||
--runtime-version=<value> Filter updates by runtime version
|
||||
|
||||
DESCRIPTION
|
||||
view the recent updates
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/list.ts)_
|
||||
|
||||
## `eas update:republish`
|
||||
|
||||
@@ -1910,7 +1960,7 @@ DESCRIPTION
|
||||
roll back to an existing update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/republish.ts)_
|
||||
|
||||
## `eas update:revert-update-rollout`
|
||||
|
||||
@@ -1937,7 +1987,7 @@ DESCRIPTION
|
||||
revert a rollout update for a project
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/revert-update-rollout.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/revert-update-rollout.ts)_
|
||||
|
||||
## `eas update:roll-back-to-embedded`
|
||||
|
||||
@@ -1965,7 +2015,7 @@ DESCRIPTION
|
||||
roll back to the embedded update
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
|
||||
|
||||
## `eas update:rollback`
|
||||
|
||||
@@ -1986,7 +2036,7 @@ DESCRIPTION
|
||||
instead execute "eas update:republish" or "eas update:roll-back-to-embedded".
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/rollback.ts)_
|
||||
|
||||
## `eas update:view GROUPID`
|
||||
|
||||
@@ -2006,7 +2056,7 @@ DESCRIPTION
|
||||
update group details
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/update/view.ts)_
|
||||
|
||||
## `eas upload`
|
||||
|
||||
@@ -2027,7 +2077,7 @@ DESCRIPTION
|
||||
upload a local build and generate a sharable link
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/upload.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/upload.ts)_
|
||||
|
||||
## `eas webhook:create`
|
||||
|
||||
@@ -2048,7 +2098,7 @@ DESCRIPTION
|
||||
create a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/webhook/create.ts)_
|
||||
|
||||
## `eas webhook:delete [ID]`
|
||||
|
||||
@@ -2068,7 +2118,7 @@ DESCRIPTION
|
||||
delete a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/webhook/delete.ts)_
|
||||
|
||||
## `eas webhook:list`
|
||||
|
||||
@@ -2086,7 +2136,7 @@ DESCRIPTION
|
||||
list webhooks
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/webhook/list.ts)_
|
||||
|
||||
## `eas webhook:update`
|
||||
|
||||
@@ -2108,7 +2158,7 @@ DESCRIPTION
|
||||
update a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/webhook/update.ts)_
|
||||
|
||||
## `eas webhook:view ID`
|
||||
|
||||
@@ -2125,7 +2175,7 @@ DESCRIPTION
|
||||
view a webhook
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/webhook/view.ts)_
|
||||
|
||||
## `eas whoami`
|
||||
|
||||
@@ -2245,7 +2295,7 @@ DESCRIPTION
|
||||
to cancel.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/cancel.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/cancel.ts)_
|
||||
|
||||
## `eas workflow:create [NAME]`
|
||||
|
||||
@@ -2265,7 +2315,7 @@ DESCRIPTION
|
||||
create a new workflow configuration YAML file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/create.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/create.ts)_
|
||||
|
||||
## `eas workflow:logs [ID]`
|
||||
|
||||
@@ -2289,7 +2339,7 @@ DESCRIPTION
|
||||
If no ID is passed in, you will be prompted to select from recent workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/logs.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/logs.ts)_
|
||||
|
||||
## `eas workflow:run [FILE]`
|
||||
|
||||
@@ -2307,7 +2357,7 @@ FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--ref=<value> Git reference to run the workflow on
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false. Cannot be used with the --json flag.
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false.
|
||||
|
||||
DESCRIPTION
|
||||
run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow
|
||||
@@ -2324,12 +2374,12 @@ FLAG DESCRIPTIONS
|
||||
When this flag is used, the local project is not uploaded; instead, the workflow is run from the exact state of the
|
||||
project at the chosen reference.
|
||||
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false. Cannot be used with the --json flag.
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false.
|
||||
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/run.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/run.ts)_
|
||||
|
||||
## `eas workflow:runs`
|
||||
|
||||
@@ -2354,7 +2404,35 @@ DESCRIPTION
|
||||
list recent workflow runs for this project, with their IDs, statuses, and timestamps
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/runs.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/runs.ts)_
|
||||
|
||||
## `eas workflow:status [WORKFLOW_RUN_ID]`
|
||||
|
||||
show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.
|
||||
|
||||
```
|
||||
USAGE
|
||||
$ eas workflow:status [WORKFLOW_RUN_ID] [--non-interactive] [--wait] [--json]
|
||||
|
||||
ARGUMENTS
|
||||
WORKFLOW_RUN_ID A workflow run ID.
|
||||
|
||||
FLAGS
|
||||
--json Enable JSON output, non-JSON messages will be printed to stderr.
|
||||
--non-interactive Run the command in non-interactive mode.
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false.
|
||||
|
||||
DESCRIPTION
|
||||
show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent
|
||||
workflow runs for the current project.
|
||||
|
||||
FLAG DESCRIPTIONS
|
||||
--[no-]wait Wait for workflow run to complete. Defaults to false.
|
||||
|
||||
Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/status.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/status.ts)_
|
||||
|
||||
## `eas workflow:validate PATH`
|
||||
|
||||
@@ -2374,7 +2452,7 @@ DESCRIPTION
|
||||
validate a workflow configuration yaml file
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/validate.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/validate.ts)_
|
||||
|
||||
## `eas workflow:view [ID]`
|
||||
|
||||
@@ -2396,5 +2474,5 @@ DESCRIPTION
|
||||
workflow runs for the current project.
|
||||
```
|
||||
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v16.24.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
_See code: [packages/eas-cli/src/commands/workflow/view.ts](https://github.com/expo/eas-cli/blob/v16.30.0/packages/eas-cli/src/commands/workflow/view.ts)_
|
||||
<!-- commandsstop -->
|
||||
Generated
+3874
-566
File diff suppressed because it is too large.
Load diff
@@ -1,25 +1,25 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "16.24.0",
|
||||
"version": "16.30.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
},
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.1.12",
|
||||
"@expo/apple-utils": "2.1.13",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.239",
|
||||
"@expo/eas-json": "16.23.0",
|
||||
"@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.7.0",
|
||||
"@expo/package-manager": "1.9.10",
|
||||
"@expo/pkcs12": "0.1.3",
|
||||
"@expo/plist": "0.2.0",
|
||||
"@expo/plugin-help": "5.1.23",
|
||||
@@ -58,6 +58,7 @@
|
||||
"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",
|
||||
@@ -71,7 +72,7 @@
|
||||
"node-forge": "1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "9.0.0",
|
||||
"ora": "5.1.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pngjs": "7.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
@@ -107,6 +108,7 @@
|
||||
"@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",
|
||||
@@ -129,7 +131,7 @@
|
||||
"rimraf": "3.0.2",
|
||||
"ts-mockito": "2.6.1",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -220,26 +222,27 @@
|
||||
"-v"
|
||||
]
|
||||
},
|
||||
"repository": "expo/eas-cli",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/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": "tsc --project tsconfig.build.json && yarn copy-new-templates",
|
||||
"build": "yarn typecheck-for-build && yarn copy-new-templates",
|
||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"watch": "yarn typecheck-for-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"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
@@ -33,6 +33,7 @@ export interface IosBuildContext {
|
||||
}
|
||||
|
||||
export interface BuildContext<T extends Platform> {
|
||||
accountId: string;
|
||||
accountName: string;
|
||||
easJsonCliConfig: EasJson['cli'];
|
||||
buildProfile: BuildProfile<T>;
|
||||
|
||||
@@ -125,6 +125,7 @@ export async function createBuildContextAsync<T extends Platform>({
|
||||
);
|
||||
|
||||
const commonContext: CommonContext<T> = {
|
||||
accountId,
|
||||
accountName: account.name,
|
||||
buildProfile,
|
||||
buildProfileName,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Job, Metadata } from '@expo/eas-build-job';
|
||||
import { Job, Metadata, version } 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 = '1.0.171';
|
||||
const PLUGIN_PACKAGE_VERSION = version; // should match version of @expo/eas-build-job
|
||||
|
||||
export enum LocalBuildMode {
|
||||
/**
|
||||
@@ -60,6 +61,7 @@ export async function runLocalBuildAsync(
|
||||
...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' }
|
||||
: {}),
|
||||
|
||||
@@ -78,10 +78,12 @@ 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;
|
||||
@@ -409,6 +411,15 @@ async function prepareAndStartBuildAsync({
|
||||
env,
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* TODO: Move to eas-build repo
|
||||
*/
|
||||
import invariant from 'invariant';
|
||||
|
||||
const EXPERIENCE_NAME_REGEX = /^@(.*?)\/(.*)/; // @<captureGroup1>/<captureGroup2>
|
||||
const SLUG_REGEX_STRICT = /^[a-zA-Z0-9_\\-]+$/; // from xdl-schemas slug
|
||||
|
||||
export const isValidExperienceName = (experienceName: string): boolean => {
|
||||
const matches = experienceName.match(EXPERIENCE_NAME_REGEX);
|
||||
return matches !== null && matches[1].length > 0 && matches[2].length > 0;
|
||||
};
|
||||
|
||||
export const experienceToAccountName = (experienceName: string): string => {
|
||||
const matches = experienceName.match(EXPERIENCE_NAME_REGEX);
|
||||
const accountName = matches ? matches[1] : null;
|
||||
invariant(accountName, `The experience name "${experienceName}" is malformed`);
|
||||
return accountName;
|
||||
};
|
||||
|
||||
export const fullNameToSlug = (fullName: string): string | null => {
|
||||
const match = fullName.match(EXPERIENCE_NAME_REGEX);
|
||||
return match ? match[2] : null;
|
||||
};
|
||||
|
||||
export const isValidSlugStrict = (slug: string): boolean => SLUG_REGEX_STRICT.test(slug);
|
||||
|
||||
// keep this list in sync with our project creation CLI https://github.com/expo/expo/blob/main/packages/create-expo/src/Template.ts
|
||||
export const FORBIDDEN_NAMES = [
|
||||
'react-native',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-native-web',
|
||||
'expo',
|
||||
'expo-router',
|
||||
];
|
||||
|
||||
export function isSlugForbidden(slug: string): boolean {
|
||||
return FORBIDDEN_NAMES.includes(slug);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LogSpy } from './testUtils';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
|
||||
import { installDependenciesAsync } from '../../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../../onboarding/runCommand';
|
||||
@@ -11,23 +10,14 @@ import {
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../onboarding/installDependencies');
|
||||
jest.mock('../../../ora');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('commands', () => {
|
||||
let logSpy: LogSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
logSpy = new LogSpy('log');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.restore();
|
||||
});
|
||||
|
||||
describe('cloneTemplateAsync', () => {
|
||||
it('should clone the template project with ssh', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
@@ -40,13 +30,12 @@ describe('commands', () => {
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'ssh',
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
@@ -63,13 +52,12 @@ describe('commands', () => {
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'https',
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
@@ -96,6 +84,7 @@ describe('commands', () => {
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', 'expo-updates'],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
@@ -103,6 +92,7 @@ describe('commands', () => {
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', '@expo/metro-runtime'],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -118,6 +108,7 @@ describe('commands', () => {
|
||||
args: ['init'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
@@ -125,6 +116,7 @@ describe('commands', () => {
|
||||
args: ['add', '.'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
@@ -132,6 +124,7 @@ describe('commands', () => {
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
findAvailableProjectNameAsync,
|
||||
generateProjectConfigAsync,
|
||||
generateProjectNameVariations,
|
||||
getAccountChoices,
|
||||
promptForProjectAccountAsync,
|
||||
} from '../configs';
|
||||
@@ -17,6 +16,9 @@ jest.mock('../../../prompts');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../utils', () => ({
|
||||
printDirectory: jest.fn((dir: string) => dir),
|
||||
}));
|
||||
|
||||
describe('configs', () => {
|
||||
let logSpy: LogSpy;
|
||||
@@ -53,49 +55,53 @@ describe('configs', () => {
|
||||
};
|
||||
|
||||
describe('when no path is provided', () => {
|
||||
it('should generate name and directory when base name is available', async () => {
|
||||
it('should prompt for project name with default value', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'expo-project' });
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'new-expo-project',
|
||||
projectDirectory: expect.stringContaining('/new-expo-project'),
|
||||
projectName: 'expo-project',
|
||||
projectDirectory: expect.stringContaining('/expo-project'),
|
||||
});
|
||||
logSpy.expectLogToContain('Using project name: new-expo-project');
|
||||
expectPromptToHaveMessage('What would you like to name your project?');
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
initial: 'expo-project',
|
||||
})
|
||||
);
|
||||
logSpy.expectLogToContain('Using project name: expo-project');
|
||||
});
|
||||
|
||||
it('should use username-date when base name exists', async () => {
|
||||
(fs.pathExists as jest.Mock)
|
||||
.mockResolvedValueOnce(true) // base name exists
|
||||
.mockResolvedValueOnce(false); // username-date doesn't exist
|
||||
it('should prompt and use custom name entered by user', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'user-custom-name' });
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^new-expo-project-jester-\d{4}-\d{2}-\d{2}$/),
|
||||
projectDirectory: expect.stringContaining(result.projectName),
|
||||
projectName: 'user-custom-name',
|
||||
projectDirectory: expect.stringContaining('/user-custom-name'),
|
||||
});
|
||||
logSpy.expectLogToContain('Using alternate project name:');
|
||||
expectPromptToHaveMessage('What would you like to name your project?');
|
||||
logSpy.expectLogToContain('Using project name: user-custom-name');
|
||||
});
|
||||
|
||||
it('should use username-date-shortID when base name and username-date exist', async () => {
|
||||
(fs.pathExists as jest.Mock)
|
||||
.mockResolvedValueOnce(true) // base name exists
|
||||
.mockResolvedValueOnce(true); // username-date exists
|
||||
it('should use shortid when prompted name exists', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true); // base name exists
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
mockUserInput({ name: 'existing-project' });
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
const result = await generateProjectConfigAsync(undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(
|
||||
/^new-expo-project-jester-\d{4}-\d{2}-\d{2}-[a-zA-Z0-9_-]{6}$/
|
||||
),
|
||||
projectName: expect.stringMatching(/^existing-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining(result.projectName),
|
||||
});
|
||||
logSpy.expectLogToContain('Using alternate project name:');
|
||||
logSpy.expectLogToContain('Using project name:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,12 +110,13 @@ describe('configs', () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
const absolutePath = '/absolute/path/to/my-project';
|
||||
const result = await generateProjectConfigAsync(jester, absolutePath, mockOptions);
|
||||
const result = await generateProjectConfigAsync(absolutePath, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-project',
|
||||
projectDirectory: absolutePath,
|
||||
});
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
|
||||
@@ -117,12 +124,13 @@ describe('configs', () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
const relativePath = 'some/relative/my-app';
|
||||
const result = await generateProjectConfigAsync(jester, relativePath, mockOptions);
|
||||
const result = await generateProjectConfigAsync(relativePath, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-app',
|
||||
projectDirectory: expect.stringContaining('some/relative/my-app'),
|
||||
});
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
});
|
||||
@@ -150,10 +158,6 @@ describe('configs', () => {
|
||||
const result = getAccountChoices(jester);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
title: 'jester (personal account)',
|
||||
value: { name: 'jester' },
|
||||
},
|
||||
{
|
||||
title: 'other',
|
||||
value: { name: 'other' },
|
||||
@@ -161,19 +165,10 @@ describe('configs', () => {
|
||||
description:
|
||||
'You do not have the required permissions to create projects on this account.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateProjectNameVariations', () => {
|
||||
it('should generate name variations', () => {
|
||||
const baseName = 'my-project';
|
||||
const result = generateProjectNameVariations(jester, baseName);
|
||||
|
||||
expect(result).toEqual([
|
||||
'my-project',
|
||||
expect.stringMatching(/^my-project-jester-\d{4}-\d{2}-\d{2}$/),
|
||||
expect.stringMatching(/^my-project-jester-\d{4}-\d{2}-\d{2}-[a-zA-Z0-9_-]{6}$/),
|
||||
{
|
||||
title: 'jester',
|
||||
value: { name: 'jester' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -189,12 +184,7 @@ describe('configs', () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'test-project',
|
||||
'/base/path',
|
||||
mockOptions
|
||||
);
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'test-project',
|
||||
@@ -208,63 +198,48 @@ describe('configs', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should try alternatives when first name exists locally', async () => {
|
||||
(fs.pathExists as jest.Mock)
|
||||
.mockResolvedValueOnce(true) // First exists
|
||||
.mockResolvedValueOnce(false); // Second available
|
||||
it('should use shortid variant when base name exists locally', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true); // Base name exists locally
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'test-project',
|
||||
'/base/path',
|
||||
mockOptions
|
||||
);
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-jester-\d{4}-\d{2}-\d{2}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-jester-'),
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(2);
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(1);
|
||||
expect(fs.pathExists).toHaveBeenCalledWith('/base/path/test-project');
|
||||
});
|
||||
|
||||
it('should skip name if local is available but remote exists', async () => {
|
||||
it('should use shortid variant when base name exists remotely', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockResolvedValueOnce('project-id') // First name exists remotely
|
||||
.mockResolvedValueOnce(null); // Second available remotely
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id'); // Base name exists remotely
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'test-project',
|
||||
'/base/path',
|
||||
mockOptions
|
||||
);
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-jester-\d{4}-\d{2}-\d{2}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-jester-'),
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(2);
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(1);
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester',
|
||||
'test-project'
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when all variations taken', async () => {
|
||||
it('should use shortid variant when base name is taken both locally and remotely', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(true);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue('project-id');
|
||||
|
||||
await expect(
|
||||
findAvailableProjectNameAsync(jester, 'test-project', '/base/path', mockOptions)
|
||||
).rejects.toThrow('Unable to find a unique project name');
|
||||
});
|
||||
const result = await findAvailableProjectNameAsync('test-project', '/base/path', mockOptions);
|
||||
|
||||
it('should log when using alternative name', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
await findAvailableProjectNameAsync(jester, 'test-project', '/base/path', mockOptions);
|
||||
|
||||
logSpy.expectLogToContain('Using alternate project name:');
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^test-project-[a-zA-Z0-9_-]{6}$/),
|
||||
projectDirectory: expect.stringContaining('/base/path/test-project-'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { LogSpy } from './testUtils';
|
||||
import { getEASUpdateURL } from '../../../api';
|
||||
import { AppFragment } from '../../../graphql/generated';
|
||||
import { easCliVersion } from '../../../utils/easCli';
|
||||
@@ -18,12 +17,6 @@ jest.mock('../../../api');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('projectFiles', () => {
|
||||
let logSpy: LogSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
logSpy = new LogSpy('withTick');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
@@ -33,10 +26,7 @@ describe('projectFiles', () => {
|
||||
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.restore();
|
||||
jest.mocked(fs.symlink).mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
describe('cleanAndPrefix', () => {
|
||||
@@ -128,8 +118,6 @@ describe('projectFiles', () => {
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/app.json`, expectedConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
logSpy.expectLogToContain('Generated app.json');
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the bundle identifier', async () => {
|
||||
@@ -247,8 +235,6 @@ describe('projectFiles', () => {
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
logSpy.expectLogToContain('Generated eas.json');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -265,7 +251,7 @@ describe('projectFiles', () => {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
preview: 'npx eas-cli@latest workflow:run publish-preview-update.yml',
|
||||
draft: 'npx eas-cli@latest workflow:run create-draft.yml',
|
||||
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
|
||||
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
|
||||
},
|
||||
@@ -274,8 +260,6 @@ describe('projectFiles', () => {
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/package.json`, expectedPackageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
logSpy.expectLogToContain('Updated package.json with scripts');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,12 +270,14 @@ describe('projectFiles', () => {
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
expect(fs.copy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('templates/.eas/workflows'),
|
||||
expect.stringContaining('.eas/workflows'),
|
||||
{ errorOnExist: false, overwrite: true }
|
||||
expect.stringContaining('templates'),
|
||||
projectDir,
|
||||
expect.objectContaining({
|
||||
errorOnExist: false,
|
||||
overwrite: true,
|
||||
filter: expect.any(Function),
|
||||
})
|
||||
);
|
||||
|
||||
logSpy.expectLogToContain('Created EAS workflow files');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -342,8 +328,6 @@ Install the dependencies first.`;
|
||||
projectReadmePath,
|
||||
expect.stringMatching(new RegExp(headings.join('[\\s\\S]*')))
|
||||
);
|
||||
|
||||
logSpy.expectLogToContain('Updated README.md with EAS configuration details');
|
||||
});
|
||||
|
||||
it('should replace npm run with package manager specific commands', async () => {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import path from 'path';
|
||||
|
||||
import { printDirectory } from '../utils';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('printDirectory', () => {
|
||||
let cwdSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock process.cwd() to return a predictable value
|
||||
cwdSpy = jest.spyOn(process, 'cwd').mockReturnValue('/Users/test/projects');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return "." for the current directory', () => {
|
||||
const result = printDirectory('/Users/test/projects');
|
||||
expect(result).toBe('.');
|
||||
});
|
||||
|
||||
it('should return relative path with "./" prefix for subdirectory', () => {
|
||||
const result = printDirectory('/Users/test/projects/my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should return relative path with "./" prefix for nested subdirectory', () => {
|
||||
const result = printDirectory('/Users/test/projects/nested/my-app');
|
||||
expect(result).toBe('./nested/my-app');
|
||||
});
|
||||
|
||||
it('should return absolute path for directory outside cwd', () => {
|
||||
const result = printDirectory('/Users/other/projects/my-app');
|
||||
expect(result).toBe('/Users/other/projects/my-app');
|
||||
});
|
||||
|
||||
it('should return absolute path for parent directory', () => {
|
||||
const result = printDirectory('/Users/test');
|
||||
expect(result).toBe('/Users/test');
|
||||
});
|
||||
|
||||
it('should handle relative paths and convert them correctly', () => {
|
||||
// Relative path resolves to absolute first, then converts back to relative
|
||||
const result = printDirectory('my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle relative paths with "./" already', () => {
|
||||
const result = printDirectory('./my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle relative paths with "../" (goes outside cwd)', () => {
|
||||
const result = printDirectory('../other-project');
|
||||
// This goes outside cwd, so should return absolute path
|
||||
expect(result).toBe('/Users/test/other-project');
|
||||
});
|
||||
|
||||
it('should handle complex relative paths', () => {
|
||||
const result = printDirectory('./nested/deep/project');
|
||||
expect(result).toBe('./nested/deep/project');
|
||||
});
|
||||
|
||||
it('should normalize paths correctly', () => {
|
||||
const result = printDirectory('/Users/test/projects/./my-app/../my-app');
|
||||
expect(result).toBe('./my-app');
|
||||
});
|
||||
|
||||
it('should handle paths in real cwd', () => {
|
||||
// Restore mock to test with real cwd
|
||||
cwdSpy.mockRestore();
|
||||
const realCwd = process.cwd();
|
||||
|
||||
// Test with current directory
|
||||
const result1 = printDirectory(realCwd);
|
||||
expect(result1).toBe('.');
|
||||
|
||||
// Test with subdirectory
|
||||
const subDir = path.join(realCwd, 'test-dir');
|
||||
const result2 = printDirectory(subDir);
|
||||
expect(result2).toBe('./test-dir');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +1,20 @@
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import { printDirectory } from './utils';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
|
||||
import { PackageManager, installDependenciesAsync } from '../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../onboarding/runCommand';
|
||||
import { ora } from '../../ora';
|
||||
|
||||
export async function cloneTemplateAsync(targetProjectDir: string): Promise<string> {
|
||||
const githubUsername = 'expo';
|
||||
const githubRepositoryName = 'expo-template-default';
|
||||
|
||||
Log.log(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
Log.newLine();
|
||||
const spinner = ora(
|
||||
`${chalk.bold(`Cloning the project to ${printDirectory(targetProjectDir)}`)}`
|
||||
).start();
|
||||
|
||||
const cloneMethod = (await canAccessRepositoryUsingSshAsync({
|
||||
githubUsername,
|
||||
@@ -25,8 +28,11 @@ export async function cloneTemplateAsync(targetProjectDir: string): Promise<stri
|
||||
githubRepositoryName,
|
||||
targetProjectDir,
|
||||
cloneMethod,
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
spinner.succeed(`Cloned the project to ${printDirectory(finalTargetProjectDirectory)}`);
|
||||
|
||||
return finalTargetProjectDirectory;
|
||||
}
|
||||
|
||||
@@ -34,6 +40,7 @@ export async function installProjectDependenciesAsync(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const spinner = ora(`${chalk.bold('Installing project dependencies')}`).start();
|
||||
await installDependenciesAsync({
|
||||
outputLevel: 'none',
|
||||
projectDir,
|
||||
@@ -42,16 +49,20 @@ export async function installProjectDependenciesAsync(
|
||||
|
||||
const dependencies = ['expo-updates', '@expo/metro-runtime'];
|
||||
for (const dependency of dependencies) {
|
||||
spinner.text = `Installing ${chalk.bold(dependency)}`;
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', dependency],
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
}
|
||||
spinner.succeed(`Installed project dependencies`);
|
||||
}
|
||||
|
||||
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
|
||||
const spinner = ora(`${chalk.bold('Initializing Git repository')}`).start();
|
||||
await fs.remove(path.join(projectDir, '.git'));
|
||||
|
||||
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
|
||||
@@ -62,7 +73,8 @@ export async function initializeGitRepositoryAsync(projectDir: string): Promise<
|
||||
command: 'git',
|
||||
args,
|
||||
showOutput: false,
|
||||
showSpinner: false,
|
||||
});
|
||||
Log.log();
|
||||
}
|
||||
spinner.succeed(`Initialized Git repository`);
|
||||
}
|
||||
@@ -2,11 +2,12 @@ import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
|
||||
import { printDirectory } from './utils';
|
||||
import { Role } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { Choice, promptAsync } from '../../prompts';
|
||||
import { Actor, getActorUsername } from '../../user/User';
|
||||
import { Actor } from '../../user/User';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
function validateProjectPath(resolvedPath: string): void {
|
||||
@@ -31,7 +32,6 @@ function validateProjectPath(resolvedPath: string): void {
|
||||
}
|
||||
|
||||
export async function generateProjectConfigAsync(
|
||||
actor: Actor,
|
||||
pathArg: string | undefined,
|
||||
options: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
@@ -41,7 +41,7 @@ export async function generateProjectConfigAsync(
|
||||
projectName: string;
|
||||
projectDirectory: string;
|
||||
}> {
|
||||
let baseName = 'new-expo-project';
|
||||
let baseName = 'expo-project';
|
||||
let parentDirectory = process.cwd();
|
||||
|
||||
if (pathArg) {
|
||||
@@ -49,17 +49,26 @@ export async function generateProjectConfigAsync(
|
||||
validateProjectPath(resolvedPath);
|
||||
baseName = path.basename(resolvedPath);
|
||||
parentDirectory = path.dirname(resolvedPath);
|
||||
} else {
|
||||
baseName = (
|
||||
await promptAsync({
|
||||
type: 'text',
|
||||
name: 'name',
|
||||
message: 'What would you like to name your project?',
|
||||
initial: 'expo-project',
|
||||
})
|
||||
).name;
|
||||
}
|
||||
|
||||
// Find an available name checking both local filesystem and remote server
|
||||
const { projectName, projectDirectory } = await findAvailableProjectNameAsync(
|
||||
actor,
|
||||
baseName,
|
||||
parentDirectory,
|
||||
options
|
||||
);
|
||||
|
||||
Log.withInfo(`Using project directory: ${projectDirectory}`);
|
||||
Log.withInfo(`Using project name: ${projectName}`);
|
||||
Log.withInfo(`Using project directory: ${printDirectory(projectDirectory)}`);
|
||||
|
||||
return {
|
||||
projectName,
|
||||
@@ -108,15 +117,18 @@ export async function promptForProjectAccountAsync(actor: Actor): Promise<string
|
||||
|
||||
export function getAccountChoices(actor: Actor, permissionsMap?: Map<string, boolean>): Choice[] {
|
||||
const permissions = permissionsMap ?? getAccountPermissionsMap(actor);
|
||||
const sortedAccounts = [...actor.accounts].sort((a, _b) =>
|
||||
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
|
||||
);
|
||||
const sortedAccounts = [...actor.accounts].sort((a, _b) => (a.ownerUserActor ? 1 : -1));
|
||||
|
||||
return sortedAccounts.map(account => {
|
||||
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
||||
const isPersonalAccount = !!account.ownerUserActor && account.ownerUserActor.id === actor.id;
|
||||
const isTeamAccount = !!account.ownerUserActor && account.ownerUserActor.id !== actor.id;
|
||||
|
||||
const accountDisplayName = isPersonalAccount
|
||||
? `${account.name} (personal account)`
|
||||
: account.name;
|
||||
? `${account.name} (Limited - Personal Account)`
|
||||
: isTeamAccount
|
||||
? `${account.name} (Limited - Team Account)`
|
||||
: account.name;
|
||||
|
||||
const disabled = !permissions.get(account.name);
|
||||
|
||||
return {
|
||||
@@ -130,17 +142,6 @@ export function getAccountChoices(actor: Actor, permissionsMap?: Map<string, boo
|
||||
});
|
||||
}
|
||||
|
||||
export function generateProjectNameVariations(actor: Actor, baseName: string): string[] {
|
||||
const username = getActorUsername(actor);
|
||||
const date = new Date().toISOString().split('T')[0];
|
||||
|
||||
return [
|
||||
baseName,
|
||||
`${baseName}-${username}-${date}`,
|
||||
`${baseName}-${username}-${date}-${nanoid(6)}`,
|
||||
];
|
||||
}
|
||||
|
||||
async function verifyProjectDoesNotExistAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountName: string,
|
||||
@@ -167,7 +168,6 @@ async function verifyProjectDoesNotExistAsync(
|
||||
* Remote server (project already exists on Expo)
|
||||
*/
|
||||
export async function findAvailableProjectNameAsync(
|
||||
actor: Actor,
|
||||
baseName: string,
|
||||
parentDirectory: string,
|
||||
{
|
||||
@@ -178,37 +178,24 @@ export async function findAvailableProjectNameAsync(
|
||||
projectAccount: string;
|
||||
}
|
||||
): Promise<{ projectName: string; projectDirectory: string }> {
|
||||
const nameVariations = generateProjectNameVariations(actor, baseName);
|
||||
let projectName = baseName;
|
||||
let projectDirectory = path.join(parentDirectory, projectName);
|
||||
|
||||
for (let i = 0; i < nameVariations.length; i++) {
|
||||
const nameVariation = nameVariations[i];
|
||||
const proposedDirectory = path.join(parentDirectory, nameVariation);
|
||||
const usingVariant = i !== 0;
|
||||
const localExists = await fs.pathExists(projectDirectory);
|
||||
|
||||
const localExists = await fs.pathExists(proposedDirectory);
|
||||
if (localExists) {
|
||||
continue;
|
||||
}
|
||||
const remoteAvailable = await verifyProjectDoesNotExistAsync(
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
projectName
|
||||
);
|
||||
|
||||
const remoteAvailable = await verifyProjectDoesNotExistAsync(
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
nameVariation,
|
||||
{ silent: usingVariant }
|
||||
);
|
||||
if (!remoteAvailable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.withInfo(`Using ${usingVariant ? 'alternate ' : ''}project name: ${nameVariation}`);
|
||||
|
||||
return {
|
||||
projectName: nameVariation,
|
||||
projectDirectory: proposedDirectory,
|
||||
};
|
||||
if (localExists || !remoteAvailable) {
|
||||
projectName = `${baseName}-${nanoid(6)}`;
|
||||
projectDirectory = path.join(parentDirectory, projectName);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to find a unique project name for "${baseName}". All generated variations already exist.`
|
||||
);
|
||||
return {
|
||||
projectName,
|
||||
projectDirectory,
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import merge from 'ts-deepmerge';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { AppFragment } from '../../graphql/generated';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { PackageManager } from '../../onboarding/installDependencies';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
|
||||
@@ -63,12 +61,6 @@ export async function generateAppConfigAsync(projectDir: string, app: AppFragmen
|
||||
|
||||
const appJsonPath = path.join(projectDir, 'app.json');
|
||||
await fs.writeJson(appJsonPath, { expo: mergedConfig }, { spaces: 2 });
|
||||
Log.withTick(
|
||||
`Generated ${chalk.bold('app.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/'
|
||||
)}`
|
||||
);
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function generateEasConfigAsync(projectDir: string): Promise<void> {
|
||||
@@ -116,12 +108,6 @@ export async function generateEasConfigAsync(projectDir: string): Promise<void>
|
||||
|
||||
const easJsonPath = path.join(projectDir, 'eas.json');
|
||||
await fs.writeJson(easJsonPath, easJson, { spaces: 2 });
|
||||
Log.withTick(
|
||||
`Generated ${chalk.bold('eas.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
)}`
|
||||
);
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function updatePackageJsonAsync(projectDir: string): Promise<void> {
|
||||
@@ -131,27 +117,25 @@ export async function updatePackageJsonAsync(projectDir: string): Promise<void>
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.yml';
|
||||
packageJson.scripts.draft = 'npx eas-cli@latest workflow:run create-draft.yml';
|
||||
packageJson.scripts['development-builds'] =
|
||||
'npx eas-cli@latest workflow:run create-development-builds.yml';
|
||||
packageJson.scripts.deploy = 'npx eas-cli@latest workflow:run deploy-to-production.yml';
|
||||
|
||||
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
||||
Log.withTick('Updated package.json with scripts');
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function copyProjectTemplatesAsync(projectDir: string): Promise<void> {
|
||||
const templatesSourceDir = path.join(__dirname, 'templates', '.eas', 'workflows');
|
||||
const easWorkflowsTargetDir = path.join(projectDir, '.eas', 'workflows');
|
||||
const templatesSourceDir = path.join(__dirname, 'templates');
|
||||
|
||||
await fs.copy(templatesSourceDir, easWorkflowsTargetDir, {
|
||||
// Copy everything from templates to projectDir, skipping readme-additions.md
|
||||
await fs.copy(templatesSourceDir, projectDir, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
filter: (src: string) => {
|
||||
return !src.endsWith('readme-additions.md');
|
||||
},
|
||||
});
|
||||
|
||||
Log.withTick('Created EAS workflow files');
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function updateReadmeAsync(
|
||||
@@ -189,7 +173,4 @@ export async function updateReadmeAsync(
|
||||
mergedReadme = mergedReadme.replaceAll('npm run', `${packageManager} run`);
|
||||
|
||||
await fs.writeFile(projectReadmePath, mergedReadme);
|
||||
|
||||
Log.withTick('Updated README.md with EAS configuration details');
|
||||
Log.log();
|
||||
}
|
||||
+4
-1
@@ -1,4 +1,4 @@
|
||||
name: Publish preview update
|
||||
name: Create draft
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -10,3 +10,6 @@ jobs:
|
||||
type: update
|
||||
params:
|
||||
branch: ${{ github.ref_name || 'test' }}
|
||||
deploy_website:
|
||||
name: Deploy website
|
||||
type: deploy
|
||||
@@ -0,0 +1,163 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is an Expo/React Native mobile application. Prioritize mobile-first patterns, performance, and cross-platform compatibility.
|
||||
|
||||
## Documentation Resources
|
||||
|
||||
When working on this project, **always consult the official Expo documentation** available at:
|
||||
|
||||
- **https://docs.expo.dev/llms.txt** - Index of all available documentation files
|
||||
- **https://docs.expo.dev/llms-full.txt** - Complete Expo documentation including Expo Router, Expo Modules API, development process
|
||||
- **https://docs.expo.dev/llms-eas.txt** - Complete EAS (Expo Application Services) documentation
|
||||
- **https://docs.expo.dev/llms-sdk.txt** - Complete Expo SDK documentation
|
||||
- **https://reactnative.dev/docs/getting-started** - Complete React Native documentation
|
||||
|
||||
These documentation files are specifically formatted for AI agents and should be your **primary reference** for:
|
||||
|
||||
- Expo APIs and best practices
|
||||
- Expo Router navigation patterns
|
||||
- EAS Build, Submit, and Update workflows
|
||||
- Expo SDK modules and their usage
|
||||
- Development and deployment processes
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
/
|
||||
├── app/ # Expo Router file-based routing
|
||||
│ ├── (tabs)/ # Tab-based navigation screens
|
||||
│ │ ├── index.tsx # Home screen
|
||||
│ │ ├── explore.tsx # Explore screen
|
||||
│ │ └── _layout.tsx # Tabs layout
|
||||
│ ├── _layout.tsx # Root layout with theme provider
|
||||
│ └── modal.tsx # Modal screen example
|
||||
├── components/ # Reusable React components
|
||||
│ ├── ui/ # UI primitives (IconSymbol, Collapsible)
|
||||
│ └── ... # Feature components (themed, haptic, parallax)
|
||||
├── constants/ # App-wide constants (theme, colors)
|
||||
├── hooks/ # Custom React hooks (color scheme, theme)
|
||||
├── assets/ # Static assets (images, fonts)
|
||||
├── scripts/ # Utility scripts (reset-project)
|
||||
├── .eas/workflows/ # EAS Workflows (CI/CD automation)
|
||||
├── app.json # Expo configuration
|
||||
├── eas.json # EAS Build/Submit configuration
|
||||
└── package.json # Dependencies and scripts
|
||||
```
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Development
|
||||
|
||||
```bash
|
||||
npx expo start # Start dev server
|
||||
npx expo start --clear # Clear cache and start dev server
|
||||
npx expo install <package> # Install packages with compatible versions
|
||||
npx expo install --check # Check which installed packages need to be updated
|
||||
npx expo install --fix # Automatically update any invalid package versions
|
||||
npm run development-builds # Create development builds (workflow)
|
||||
npm run reset-project # Reset to blank template
|
||||
```
|
||||
|
||||
### Building & Testing
|
||||
|
||||
```bash
|
||||
npx expo doctor # Check project health and dependencies
|
||||
npx expo lint # Run ESLint
|
||||
npm run draft # Publish preview update and website (workflow)
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```bash
|
||||
npx eas-cli@latest build --platform ios -s # Use EAS to build for iOS platform and submit to App Store
|
||||
npx eas-cli@latest build --platform android -s # Use EAS to build for Android platform and submit to Google Play Store
|
||||
npm run deploy # Deploy to production (workflow)
|
||||
```
|
||||
|
||||
## Development Guidelines
|
||||
|
||||
### Code Style & Standards
|
||||
|
||||
- **TypeScript First**: Use TypeScript for all new code with strict type checking
|
||||
- **Naming Conventions**: Use meaningful, descriptive names for variables, functions, and components
|
||||
- **Self-Documenting Code**: Write clear, readable code that explains itself; only add comments for complex business logic or design decisions
|
||||
- **React 19 Patterns**: Follow modern React patterns including:
|
||||
- Function components with hooks
|
||||
- Enable React Compiler
|
||||
- Proper dependency arrays in useEffect
|
||||
- Memoization when appropriate (useMemo, useCallback)
|
||||
- Error boundaries for better error handling
|
||||
|
||||
### Navigation & Routing
|
||||
|
||||
- Use **Expo Router** for all navigation
|
||||
- Import `Link`, `router`, and `useLocalSearchParams` from `expo-router`
|
||||
- Docs: https://docs.expo.dev/router/introduction/
|
||||
|
||||
### Recommended Libraries
|
||||
|
||||
- **Navigation**: `expo-router` for navigation
|
||||
- **Images**: `expo-image` for optimized image handling and caching
|
||||
- **Animations**: `react-native-reanimated` for performant animations on native thread
|
||||
- **Gestures**: `react-native-gesture-handler` for native gesture recognition
|
||||
- **Storage**: Use `expo-sqlite` for persistent storage, `expo-sqlite/kv-store` for simple key-value storage
|
||||
|
||||
## Debugging & Development Tools
|
||||
|
||||
### DevTools Integration
|
||||
|
||||
- **React Native DevTools**: Use MCP `open_devtools` command to launch debugging tools
|
||||
- **Network Inspection**: Monitor API calls and network requests in DevTools
|
||||
- **Element Inspector**: Debug component hierarchy and styles
|
||||
- **Performance Profiler**: Identify performance bottlenecks
|
||||
- **Logging**: Use `console.log` for debugging (remove before production), `console.warn` for deprecation notices, `console.error` for actual errors, and implement error boundaries for production error handling
|
||||
|
||||
### Testing & Quality Assurance
|
||||
|
||||
#### Automated Testing with MCP Tools
|
||||
|
||||
Developers can configure the Expo MCP server with the following doc: https://docs.expo.dev/eas/ai/mcp/
|
||||
|
||||
- **Component Testing**: Add `testID` props to components for automation
|
||||
- **Visual Testing**: Use MCP `automation_take_screenshot` to verify UI appearance
|
||||
- **Interaction Testing**: Use MCP `automation_tap_by_testid` to simulate user interactions
|
||||
- **View Verification**: Use MCP `automation_find_view_by_testid` to validate component rendering
|
||||
|
||||
## EAS Workflows CI/CD
|
||||
|
||||
This project is pre-configured with **EAS Workflows** for automating development and release processes. Workflows are defined in `.eas/workflows/` directory.
|
||||
|
||||
When working with EAS Workflows, **always refer to**:
|
||||
|
||||
- https://docs.expo.dev/eas/workflows/ for workflow examples
|
||||
- The `.eas/workflows/` directory for existing workflow configurations
|
||||
- You can check that a workflow YAML is valid using the workflows schema: https://exp.host/--/api/v2/workflows/schema
|
||||
|
||||
### Build Profiles (eas.json)
|
||||
|
||||
- **development**: Development builds with dev client
|
||||
- **development-simulator**: Development builds for iOS simulator
|
||||
- **preview**: Internal distribution preview builds
|
||||
- **production**: Production builds with auto-increment
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Expo Go Errors & Development Builds
|
||||
|
||||
If there are errors in **Expo Go** or the project is not running, create a **development build**. **Expo Go** is a sandbox environment with a limited set of native modules. To create development builds, run `eas build:dev`. Additionally, after installing new packages or adding config plugins, new development builds are often required.
|
||||
|
||||
## AI Agent Instructions
|
||||
|
||||
When working on this project:
|
||||
|
||||
1. **Always start by consulting the appropriate documentation**:
|
||||
|
||||
- For general Expo questions: https://docs.expo.dev/llms-full.txt
|
||||
- For EAS/deployment questions: https://docs.expo.dev/llms-eas.txt
|
||||
- For SDK/API questions: https://docs.expo.dev/llms-sdk.txt
|
||||
|
||||
2. **Understand before implementing**: Read the relevant docs section before writing code
|
||||
|
||||
3. **Follow existing patterns**: Look at existing components and screens for patterns to follow
|
||||
@@ -0,0 +1,9 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
**Note**: This project uses `AGENTS.md` files for detailed guidance.
|
||||
|
||||
## Primary Reference
|
||||
|
||||
Please see `AGENTS.md` in this same directory for the main project documentation and guidance.
|
||||
@@ -21,7 +21,7 @@ This project is configured to use [EAS Workflows](https://docs.expo.dev/eas/work
|
||||
|
||||
### Previews
|
||||
|
||||
Run `npm run preview` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
|
||||
Run `npm run draft` to [publish a preview update](https://docs.expo.dev/eas/workflows/examples/publish-preview-update/) of your project, which can be viewed in Expo Go or in a development build.
|
||||
|
||||
### Development Builds
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Formats a directory path for display.
|
||||
* If the directory is within the current working directory, returns a relative path.
|
||||
* Otherwise, returns the absolute path.
|
||||
*/
|
||||
export function printDirectory(directory: string): string {
|
||||
const cwd = process.cwd();
|
||||
const absoluteDir = path.isAbsolute(directory) ? directory : path.resolve(cwd, directory);
|
||||
|
||||
const relativePath = path.relative(cwd, absoluteDir);
|
||||
|
||||
// If the relative path doesn't start with '..' it means it's within or at the cwd
|
||||
if (!relativePath.startsWith('..') && !path.isAbsolute(relativePath)) {
|
||||
return relativePath !== '' ? `./${relativePath}` : '.';
|
||||
}
|
||||
|
||||
return absoluteDir;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { isSlugForbidden, isValidExperienceName, isValidSlugStrict } from './experienceParser';
|
||||
|
||||
/**
|
||||
* Perform the same validation as the server, to avoid GraphQL errors.
|
||||
*/
|
||||
export function validateFullNameAndSlug(fullName: string, slug: string): void {
|
||||
if (!isValidExperienceName(fullName)) {
|
||||
throw new Error(`Invalid project name: ${fullName}`);
|
||||
}
|
||||
|
||||
if (!isValidSlugStrict(slug)) {
|
||||
throw new Error(`Invalid slug: ${slug}`);
|
||||
}
|
||||
|
||||
if (isSlugForbidden(slug)) {
|
||||
throw new Error(
|
||||
`Cannot create an app named "${slug}" because it would conflict with a dependency of the same name.`
|
||||
);
|
||||
}
|
||||
|
||||
if (fullName.length > 255) {
|
||||
throw new Error(`Project full name (${fullName}) can not be longer than 255 characters.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to derive a valid slug name from the one passed in
|
||||
*/
|
||||
export function validSlugName(slug: string): string {
|
||||
const chars = slug.split('');
|
||||
const validChars = chars.filter(char => isValidSlugStrict(char));
|
||||
return validChars.join('');
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { fetchRawLogsForBuildJobAsync, fetchRawLogsForCustomJobAsync } from './fetchLogs';
|
||||
@@ -11,14 +12,18 @@ import {
|
||||
import {
|
||||
WorkflowJobStatus,
|
||||
WorkflowJobType,
|
||||
WorkflowRunByIdQuery,
|
||||
WorkflowRunByIdWithJobsQuery,
|
||||
WorkflowRunFragment,
|
||||
WorkflowRunStatus,
|
||||
WorkflowRunTriggerEventType,
|
||||
} from '../../graphql/generated';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { Choice } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
export function computeTriggerInfoForWorkflowRun(run: WorkflowRunFragment): {
|
||||
@@ -218,10 +223,12 @@ export async function infoForActiveWorkflowRunAsync(
|
||||
|
||||
export async function infoForFailedWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'],
|
||||
maxLogLines: number = -1 // -1 means no limit
|
||||
): Promise<string> {
|
||||
const statusLines = [];
|
||||
const statusValues = [];
|
||||
const logLinesToKeep = maxLogLines === -1 ? Infinity : maxLogLines;
|
||||
for (const job of workflowRun.jobs) {
|
||||
if (job.status !== WorkflowJobStatus.Failure) {
|
||||
continue;
|
||||
@@ -233,7 +240,7 @@ export async function infoForFailedWorkflowRunAsync(
|
||||
if (steps.length > 0) {
|
||||
const failedStep = steps.find(step => step.status === 'fail');
|
||||
if (failedStep) {
|
||||
const logs = failedStep.logLines?.map(line => line.msg) ?? [];
|
||||
const logs = failedStep.logLines?.map(line => line.msg).slice(-logLinesToKeep) ?? [];
|
||||
statusValues.push({ label: ' Failed step', value: failedStep.name });
|
||||
statusValues.push({
|
||||
label: ' Logs for failed step',
|
||||
@@ -283,3 +290,84 @@ export async function maybeReadStdinAsync(): Promise<string | null> {
|
||||
});
|
||||
});
|
||||
}
|
||||
export async function showWorkflowStatusAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
workflowRunId,
|
||||
spinnerUsesStdErr,
|
||||
waitForCompletion = true,
|
||||
}: { workflowRunId: string; spinnerUsesStdErr: boolean; waitForCompletion?: boolean }
|
||||
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
|
||||
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
|
||||
|
||||
const spinner = ora({
|
||||
stream: spinnerUsesStdErr ? process.stderr : process.stdout,
|
||||
text: '',
|
||||
}).start();
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting to start:}`;
|
||||
|
||||
let failedFetchesCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
|
||||
failedFetchesCount = 0;
|
||||
|
||||
switch (workflowRun.status) {
|
||||
case WorkflowRunStatus.New:
|
||||
break;
|
||||
case WorkflowRunStatus.InProgress: {
|
||||
spinner.prefixText = chalk`{bold.green Workflow run is in progress:}`;
|
||||
spinner.text = await infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, 5);
|
||||
break;
|
||||
}
|
||||
case WorkflowRunStatus.ActionRequired:
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting for action:}`;
|
||||
break;
|
||||
|
||||
case WorkflowRunStatus.Canceled:
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow has been canceled.}`;
|
||||
spinner.stopAndPersist();
|
||||
return workflowRun;
|
||||
|
||||
case WorkflowRunStatus.Failure: {
|
||||
spinner.prefixText = chalk`{bold.red Workflow has failed.}`;
|
||||
const failedInfo = await infoForFailedWorkflowRunAsync(graphqlClient, workflowRun, 30);
|
||||
spinner.fail(failedInfo);
|
||||
return workflowRun;
|
||||
}
|
||||
case WorkflowRunStatus.Success:
|
||||
spinner.prefixText = chalk`{bold.green Workflow has completed successfully.}`;
|
||||
spinner.text = '';
|
||||
spinner.succeed('');
|
||||
return workflowRun;
|
||||
}
|
||||
if (!waitForCompletion) {
|
||||
if (spinner.isSpinning) {
|
||||
spinner.stopAndPersist();
|
||||
}
|
||||
return workflowRun;
|
||||
}
|
||||
} catch {
|
||||
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
|
||||
|
||||
failedFetchesCount += 1;
|
||||
|
||||
if (failedFetchesCount > 6) {
|
||||
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
|
||||
process.exit(workflowRunExitCodes.WAIT_ABORTED);
|
||||
}
|
||||
}
|
||||
|
||||
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
|
||||
}
|
||||
}
|
||||
|
||||
export const workflowRunExitCodes = {
|
||||
WORKFLOW_FAILED: 11,
|
||||
WORKFLOW_CANCELED: 12,
|
||||
WAIT_ABORTED: 13,
|
||||
};
|
||||
@@ -1,23 +1,19 @@
|
||||
import chalk from 'chalk';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { scheduleBranchDeletionAsync } from '../../branch/delete';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateBranchMutation,
|
||||
DeleteUpdateBranchMutationVariables,
|
||||
DeleteUpdateBranchResult,
|
||||
GetBranchInfoQuery,
|
||||
GetBranchInfoQueryVariables,
|
||||
} from '../../graphql/generated';
|
||||
import { GetBranchInfoQuery, GetBranchInfoQueryVariables } from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
|
||||
|
||||
async function getBranchInfoAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
@@ -50,31 +46,6 @@ async function getBranchInfoAsync(
|
||||
return data;
|
||||
}
|
||||
|
||||
async function deleteBranchOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ branchId }: DeleteUpdateBranchMutationVariables
|
||||
): Promise<DeleteUpdateBranchResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateBranchMutation, DeleteUpdateBranchMutationVariables>(
|
||||
gql`
|
||||
mutation DeleteUpdateBranch($branchId: ID!) {
|
||||
updateBranch {
|
||||
deleteUpdateBranch(branchId: $branchId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
branchId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateBranch.deleteUpdateBranch;
|
||||
}
|
||||
|
||||
export default class BranchDelete extends EasCommand {
|
||||
static override description = 'delete a branch';
|
||||
|
||||
@@ -146,12 +117,12 @@ export default class BranchDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const deletionResult = await deleteBranchOnAppAsync(graphqlClient, {
|
||||
branchId,
|
||||
});
|
||||
const receipt = await scheduleBranchDeletionAsync(graphqlClient, { branchId });
|
||||
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
|
||||
Log.debug('Deletion result', { successfulReceipt });
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
printJsonOnlyOutput({ id: branchId });
|
||||
} else {
|
||||
Log.withTick(
|
||||
`️Deleted branch "${branchName}" and all of its updates on project ${chalk.bold(
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { scheduleChannelDeletionAsync } from '../../channel/delete';
|
||||
import { selectChannelOnAppAsync } from '../../channel/queries';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import { withErrorHandlingAsync } from '../../graphql/client';
|
||||
import {
|
||||
DeleteUpdateChannelMutation,
|
||||
DeleteUpdateChannelMutationVariables,
|
||||
DeleteUpdateChannelResult,
|
||||
} from '../../graphql/generated';
|
||||
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
|
||||
import Log from '../../log';
|
||||
import { toggleConfirmAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
|
||||
|
||||
export default class ChannelDelete extends EasCommand {
|
||||
static override description = 'Delete a channel';
|
||||
@@ -77,7 +70,7 @@ export default class ChannelDelete extends EasCommand {
|
||||
if (!nonInteractive) {
|
||||
Log.addNewLineIfNone();
|
||||
Log.warn(
|
||||
`You are about to permanently delete channel: "${channelName}".\nThis action is irreversible.`
|
||||
`You are about to permanently delete channel: "${channelName}". Deleting this channel will also permanently remove all associated builds from your account. \nThis action is irreversible.`
|
||||
);
|
||||
Log.newLine();
|
||||
const confirmed = await toggleConfirmAsync({ message: 'Are you sure you wish to proceed?' });
|
||||
@@ -87,39 +80,14 @@ export default class ChannelDelete extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
const deletionResult = await deleteChannelOnAppAsync(graphqlClient, {
|
||||
channelId,
|
||||
});
|
||||
const receipt = await scheduleChannelDeletionAsync(graphqlClient, { channelId });
|
||||
const successfulReceipt = await pollForBackgroundJobReceiptAsync(graphqlClient, receipt);
|
||||
Log.debug('Deletion result', { successfulReceipt });
|
||||
|
||||
if (jsonFlag) {
|
||||
printJsonOnlyOutput(deletionResult);
|
||||
printJsonOnlyOutput({ id: channelId });
|
||||
} else {
|
||||
Log.withTick(`️Deleted channel "${channelName}".`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChannelOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ channelId }: DeleteUpdateChannelMutationVariables
|
||||
): Promise<DeleteUpdateChannelResult> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.mutation<DeleteUpdateChannelMutation, DeleteUpdateChannelMutationVariables>(
|
||||
gql`
|
||||
mutation DeleteUpdateChannel($channelId: ID!) {
|
||||
updateChannel {
|
||||
deleteUpdateChannel(channelId: $channelId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
channelId,
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.updateChannel.deleteUpdateChannel;
|
||||
}
|
||||
@@ -44,8 +44,6 @@ export default class WorkerAliasDelete extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -73,8 +73,6 @@ export default class WorkerAlias extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -45,8 +45,6 @@ export default class WorkerDelete extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -120,8 +120,6 @@ export default class WorkerDeploy extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
Log.warn('EAS Hosting is still in preview and subject to changes.');
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
@@ -26,6 +27,7 @@ jest.mock('../../../prompts');
|
||||
jest.mock('../../../utils/prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe(EnvUpdate, () => {
|
||||
const projectId = 'test-project-id';
|
||||
@@ -251,4 +253,251 @@ describe(EnvUpdate, () => {
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
});
|
||||
});
|
||||
|
||||
it('processes file type variable in non-interactive mode with --type file and --value', async () => {
|
||||
const testFilePath = '/path/to/test-file.json';
|
||||
const testFileBase64 = 'dGVzdCBmaWxlIGNvbnRlbnQ=';
|
||||
const testFileName = 'test-file.json';
|
||||
|
||||
const mockVariable = {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [DefaultEnvironment.Development],
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
const mockUpdatedVariable = {
|
||||
...mockVariable,
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
};
|
||||
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
|
||||
|
||||
// Mock fs.pathExists to return true
|
||||
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(true));
|
||||
// Mock fs.readFile to return base64 encoded content
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(testFileBase64));
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--type',
|
||||
'file',
|
||||
'--value',
|
||||
testFilePath,
|
||||
'--environment',
|
||||
'production',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
// Verify file was checked for existence
|
||||
expect(fs.pathExists).toHaveBeenCalledWith(testFilePath);
|
||||
// Verify file was read as base64
|
||||
expect(fs.readFile).toHaveBeenCalledWith(testFilePath, 'base64');
|
||||
|
||||
// Verify update was called with FileBase64 type and base64-encoded value
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: variableId,
|
||||
value: testFileBase64,
|
||||
environments: [DefaultEnvironment.Production],
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
fileName: testFileName,
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when updating FILE type variable with value that is not a valid file path', async () => {
|
||||
const plainStringValue = 'some-new-value';
|
||||
|
||||
const mockVariable = {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [DefaultEnvironment.Development],
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
};
|
||||
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
|
||||
|
||||
// Mock fs.pathExists to return false (file doesn't exist)
|
||||
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(false));
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--value',
|
||||
plainStringValue,
|
||||
'--environment',
|
||||
'production',
|
||||
// Note: --type is NOT specified
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
|
||||
// Should throw error when FILE type variable is updated with non-file value
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
`Variable "TEST_VARIABLE" is a file type, but "${plainStringValue}" does not exist as a file. If you want to convert it to a string, pass --type string.`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error when --type file is explicitly specified but file does not exist', async () => {
|
||||
const nonExistentFilePath = '/path/to/nonexistent-file.json';
|
||||
|
||||
const mockVariable = {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [DefaultEnvironment.Development],
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
|
||||
|
||||
// Mock fs.pathExists to return false (file doesn't exist)
|
||||
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(false));
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'TEST_VARIABLE',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--type',
|
||||
'file',
|
||||
'--value',
|
||||
nonExistentFilePath,
|
||||
'--environment',
|
||||
'production',
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
|
||||
// Should throw error when --type file is explicitly specified
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
`File "${nonExistentFilePath}" does not exist`
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves existing STRING type when updating value without --type flag', async () => {
|
||||
const stringValue = 'my-best-file.json';
|
||||
|
||||
const mockVariable = {
|
||||
id: variableId,
|
||||
name: 'MY_VAR',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [DefaultEnvironment.Development],
|
||||
type: EnvironmentSecretType.String,
|
||||
value: 'old-value',
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
};
|
||||
|
||||
const mockUpdatedVariable = {
|
||||
...mockVariable,
|
||||
value: stringValue,
|
||||
};
|
||||
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'MY_VAR',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--value',
|
||||
stringValue,
|
||||
// Note: --type is NOT specified
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
// Verify update was called with preserved STRING type
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: variableId,
|
||||
value: stringValue,
|
||||
type: EnvironmentSecretType.String,
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves existing FILE type when updating value without --type flag', async () => {
|
||||
const testFilePath = '/path/to/test-file.json';
|
||||
const testFileBase64 = 'dGVzdCBmaWxlIGNvbnRlbnQ=';
|
||||
const testFileName = 'test-file.json';
|
||||
|
||||
const mockVariable = {
|
||||
id: variableId,
|
||||
name: 'MY_FILE_VAR',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [DefaultEnvironment.Development],
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
value: 'old-base64-value',
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
};
|
||||
|
||||
const mockUpdatedVariable = {
|
||||
...mockVariable,
|
||||
value: testFileBase64,
|
||||
};
|
||||
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue([mockVariable]);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockUpdatedVariable);
|
||||
|
||||
// Mock fs.pathExists to return true
|
||||
jest.mocked(fs.pathExists).mockImplementation(() => Promise.resolve(true));
|
||||
// Mock fs.readFile to return base64 encoded content
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(testFileBase64));
|
||||
|
||||
const command = new EnvUpdate(
|
||||
[
|
||||
'--variable-name',
|
||||
'MY_FILE_VAR',
|
||||
'--variable-environment',
|
||||
'development',
|
||||
'--non-interactive',
|
||||
'--value',
|
||||
testFilePath,
|
||||
// Note: --type is NOT specified
|
||||
],
|
||||
mockConfig
|
||||
);
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
// Verify file was checked for existence
|
||||
expect(fs.pathExists).toHaveBeenCalledWith(testFilePath);
|
||||
// Verify file was read as base64
|
||||
expect(fs.readFile).toHaveBeenCalledWith(testFilePath, 'base64');
|
||||
|
||||
// Verify update was called with preserved FILE type
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
id: variableId,
|
||||
value: testFileBase64,
|
||||
type: EnvironmentSecretType.FileBase64,
|
||||
fileName: testFileName,
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -107,7 +107,7 @@ export default class EnvGet extends EasCommand {
|
||||
let variable;
|
||||
|
||||
if (variables.length > 1) {
|
||||
const variableInEnvironment = variables.find(v => v.environments?.includes(environment!));
|
||||
const variableInEnvironment = variables.find(v => v.environments?.includes(environment));
|
||||
if (!variableInEnvironment) {
|
||||
throw new Error(
|
||||
`Variable with name "${name}" not found in environment "${environment.toLocaleLowerCase()}"`
|
||||
|
||||
+20
-12
@@ -299,18 +299,6 @@ export default class EnvUpdate extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
let environmentFilePath: string | undefined;
|
||||
|
||||
if ((newType ?? selectedVariable.type) === EnvironmentSecretType.FileBase64 && value) {
|
||||
environmentFilePath = path.resolve(value);
|
||||
if (!(await fs.pathExists(environmentFilePath))) {
|
||||
throw new Error(`File "${value}" does not exist`);
|
||||
}
|
||||
fileName = path.basename(environmentFilePath);
|
||||
}
|
||||
|
||||
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
|
||||
|
||||
if (!environments || environments.length === 0) {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
@@ -342,6 +330,26 @@ export default class EnvUpdate extends EasCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// If value is provided but type is not explicitly set, preserve the existing type
|
||||
if (value && !newType) {
|
||||
newType = selectedVariable.type;
|
||||
}
|
||||
|
||||
if (newType === EnvironmentSecretType.FileBase64 && value) {
|
||||
const environmentFilePath = path.resolve(value);
|
||||
if (!(await fs.pathExists(environmentFilePath))) {
|
||||
if (type === 'file') {
|
||||
throw new Error(`File "${value}" does not exist`);
|
||||
} else {
|
||||
throw new Error(
|
||||
`Variable "${selectedVariable.name}" is a file type, but "${value}" does not exist as a file. If you want to convert it to a string, pass --type string.`
|
||||
);
|
||||
}
|
||||
}
|
||||
fileName = path.basename(environmentFilePath);
|
||||
value = await fs.readFile(environmentFilePath, 'base64');
|
||||
}
|
||||
|
||||
if (visibility) {
|
||||
newVisibility = parseVisibility(visibility);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,15 @@ jest.mock('../../../project/expoConfig', () => ({
|
||||
|
||||
jest.mock('../../../prompts', () => ({
|
||||
...jest.requireActual('../../../prompts'),
|
||||
promptAsync: jest.fn().mockResolvedValue({ account: { name: 'jester' } }),
|
||||
promptAsync: jest.fn().mockImplementation(async (options: any) => {
|
||||
if (options.name === 'account') {
|
||||
return { account: { name: 'jester' } };
|
||||
}
|
||||
if (options.name === 'name') {
|
||||
return { name: options.initial || 'expo-project' };
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
}));
|
||||
|
||||
describe(New.name, () => {
|
||||
@@ -62,8 +70,8 @@ describe(New.name, () => {
|
||||
|
||||
expect(result).toEqual({
|
||||
projectAccount: 'jester',
|
||||
projectDirectory: expect.stringContaining('/new-expo-project'),
|
||||
projectName: 'new-expo-project',
|
||||
projectDirectory: expect.stringContaining('/expo-project'),
|
||||
projectName: 'expo-project',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { saveProjectIdToAppConfigAsync } from '../../commandUtils/context/contextUtils/getProjectIdAsync';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { validSlugName, validateFullNameAndSlug } from '../../commandUtils/projectNameValidation';
|
||||
import { Role } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
@@ -276,8 +277,9 @@ export default class ProjectInit extends EasCommand {
|
||||
throw new Error('No account selected for project. Canceling.');
|
||||
}
|
||||
|
||||
const projectName = exp.slug;
|
||||
const projectName = validSlugName(exp.slug); // This filters out invalid characters
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
validateFullNameAndSlug(projectFullName, projectName);
|
||||
const existingProjectIdOnServer = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
accountName,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
updatePackageJsonAsync,
|
||||
updateReadmeAsync,
|
||||
} from '../../commandUtils/new/projectFiles';
|
||||
import { printDirectory } from '../../commandUtils/new/utils';
|
||||
import { AppFragment } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
@@ -40,7 +41,7 @@ export async function generateConfigsAsync(
|
||||
projectAccount: string;
|
||||
}> {
|
||||
const projectAccount = await promptForProjectAccountAsync(actor);
|
||||
const { projectName, projectDirectory } = await generateProjectConfigAsync(actor, args.path, {
|
||||
const { projectName, projectDirectory } = await generateProjectConfigAsync(args.path, {
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
});
|
||||
@@ -92,7 +93,7 @@ export async function createProjectAsync({
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
Log.withTick(`Project successfully linked (ID: ${chalk.bold(projectId)}) (modified app.json)`);
|
||||
Log.withInfo(`Project successfully linked (ID: ${chalk.bold(projectId)})`);
|
||||
|
||||
return projectId;
|
||||
}
|
||||
@@ -102,6 +103,7 @@ export async function generateProjectFilesAsync(
|
||||
app: AppFragment,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const spinner = ora(`Generating project files`).start();
|
||||
await generateAppConfigAsync(projectDir, app);
|
||||
|
||||
await generateEasConfigAsync(projectDir);
|
||||
@@ -111,6 +113,17 @@ export async function generateProjectFilesAsync(
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
await updateReadmeAsync(projectDir, packageManager);
|
||||
spinner.succeed(`Generated project files`);
|
||||
Log.withInfo(
|
||||
`Generated ${chalk.bold('app.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/versions/latest/config/app/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Generated ${chalk.bold('eas.json')}. ${learnMore(
|
||||
'https://docs.expo.dev/build-reference/eas-json/'
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
export default class New extends EasCommand {
|
||||
@@ -136,8 +149,6 @@ export default class New extends EasCommand {
|
||||
}),
|
||||
};
|
||||
|
||||
static override hidden = true;
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
@@ -155,11 +166,7 @@ export default class New extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
'This command is not yet implemented. It will create a new project, but it may not be fully configured.'
|
||||
);
|
||||
Log.log(`👋 Welcome to Expo, ${actor.username}!`);
|
||||
Log.newLine();
|
||||
|
||||
const {
|
||||
projectName,
|
||||
@@ -186,19 +193,21 @@ export default class New extends EasCommand {
|
||||
await initializeGitRepositoryAsync(projectDirectory);
|
||||
|
||||
Log.log('🎉 We finished creating your new project.');
|
||||
Log.newLine();
|
||||
Log.log('Next steps:');
|
||||
Log.withInfo(`Run \`cd ${projectDirectory}\` to navigate to your project.`);
|
||||
Log.withInfo(
|
||||
`Run \`${packageManager} run preview\` to create a preview build on EAS. ${learnMore(
|
||||
`Run ${chalk.bold(`cd ${printDirectory(projectDirectory)}`)} to navigate to your project.`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Run ${chalk.bold(`${packageManager} run draft`)} to create a preview on EAS. ${learnMore(
|
||||
'https://docs.expo.dev/eas/workflows/examples/publish-preview-update/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Run \`${packageManager} run start\` to start developing locally. ${learnMore(
|
||||
`Run ${chalk.bold(`${packageManager} run start`)} to start developing locally. ${learnMore(
|
||||
'https://docs.expo.dev/get-started/start-developing/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(`See the README.md for more information about your project.`);
|
||||
Log.newLine();
|
||||
Log.withInfo(`See the ${chalk.bold('README.md')} for more information about your project.`);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import { instance, mock } from 'ts-mockito';
|
||||
|
||||
import UpdatePublish from '..';
|
||||
import UpdatePublish, { preprocessSourceMapsArg } from '..';
|
||||
import { ensureBranchExistsAsync } from '../../../branch/queries';
|
||||
import {
|
||||
DynamicPrivateProjectConfigContextField,
|
||||
@@ -328,3 +328,34 @@ function mockTestExport({
|
||||
|
||||
return { inputDir: exportDir, platforms, runtimeVersion };
|
||||
}
|
||||
|
||||
describe(preprocessSourceMapsArg, () => {
|
||||
it('passes through when --source-maps has a value', () => {
|
||||
const argv = ['--source-maps', 'inline', '--message', 'test'];
|
||||
expect(preprocessSourceMapsArg(argv)).toEqual(argv);
|
||||
});
|
||||
|
||||
it('inserts "true" when --source-maps has no value', () => {
|
||||
// At end of args
|
||||
expect(preprocessSourceMapsArg(['--branch', 'main', '--source-maps'])).toEqual([
|
||||
'--branch',
|
||||
'main',
|
||||
'--source-maps',
|
||||
'true',
|
||||
]);
|
||||
// Followed by another flag
|
||||
expect(preprocessSourceMapsArg(['--source-maps', '--message', 'test'])).toEqual([
|
||||
'--source-maps',
|
||||
'true',
|
||||
'--message',
|
||||
'test',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hidden flags', () => {
|
||||
it('has --no-bytecode and --source-maps as hidden flags', () => {
|
||||
expect(UpdatePublish.flags['no-bytecode'].hidden).toBe(true);
|
||||
expect(UpdatePublish.flags['source-maps'].hidden).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -168,7 +168,7 @@ export default class UpdateEdit extends EasCommand {
|
||||
return await PublishMutation.setRolloutPercentageAsync(
|
||||
graphqlClient,
|
||||
u.updateId,
|
||||
rolloutPercentageToSet!
|
||||
rolloutPercentageToSet
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
@@ -31,7 +31,6 @@ import { maybeUploadFingerprintAsync } from '../../project/maybeUploadFingerprin
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
RawAsset,
|
||||
UpdatePublishPlatform,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
@@ -51,7 +50,12 @@ import {
|
||||
} from '../../project/publish';
|
||||
import { resolveWorkflowPerPlatformAsync } from '../../project/workflow';
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { getUpdateJsonInfosForUpdates } from '../../update/utils';
|
||||
import {
|
||||
UpdatePublishPlatform,
|
||||
getUpdateJsonInfosForUpdates,
|
||||
isBundleDiffingEnabled,
|
||||
prewarmDiffingAsync,
|
||||
} from '../../update/utils';
|
||||
import {
|
||||
checkManifestBodyAgainstUpdateInfoGroup,
|
||||
getCodeSigningInfoAsync,
|
||||
@@ -64,6 +68,28 @@ import formatFields from '../../utils/formatFields';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
/**
|
||||
* Preprocess argv to handle --source-maps with optional value.
|
||||
* If --source-maps is followed by another flag (starts with -) or end of args,
|
||||
* insert 'true' as the default value.
|
||||
*/
|
||||
export function preprocessSourceMapsArg(argv: string[]): string[] {
|
||||
const result: string[] = [];
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const arg = argv[i];
|
||||
result.push(arg);
|
||||
|
||||
if (arg === '--source-maps') {
|
||||
const nextArg = argv[i + 1];
|
||||
// If no next arg or next arg is a flag, insert 'true' as the value
|
||||
if (nextArg === undefined || nextArg.startsWith('-')) {
|
||||
result.push('true');
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
type RawUpdateFlags = {
|
||||
auto: boolean;
|
||||
branch?: string;
|
||||
@@ -73,6 +99,8 @@ type RawUpdateFlags = {
|
||||
'input-dir': string;
|
||||
'skip-bundler': boolean;
|
||||
'clear-cache': boolean;
|
||||
'no-bytecode': boolean;
|
||||
'source-maps'?: string;
|
||||
'private-key-path'?: string;
|
||||
'emit-metadata': boolean;
|
||||
'rollout-percentage'?: number;
|
||||
@@ -90,6 +118,8 @@ type UpdateFlags = {
|
||||
inputDir: string;
|
||||
skipBundler: boolean;
|
||||
clearCache: boolean;
|
||||
noBytecode: boolean;
|
||||
sourceMaps?: string;
|
||||
privateKeyPath?: string;
|
||||
emitMetadata: boolean;
|
||||
rolloutPercentage?: number;
|
||||
@@ -128,6 +158,16 @@ export default class UpdatePublish extends EasCommand {
|
||||
description: `Clear the bundler cache before publishing`,
|
||||
default: false,
|
||||
}),
|
||||
'no-bytecode': Flags.boolean({
|
||||
description: `Skip generating Hermes bytecode (output plain JavaScript instead)`,
|
||||
default: false,
|
||||
hidden: true,
|
||||
}),
|
||||
'source-maps': Flags.string({
|
||||
description: `Emit source maps. Options: true (default), inline, false`,
|
||||
default: 'true',
|
||||
hidden: true,
|
||||
}),
|
||||
'emit-metadata': Flags.boolean({
|
||||
description: `Emit "eas-update-metadata.json" in the bundle folder with detailed information about the generated updates`,
|
||||
default: false,
|
||||
@@ -165,7 +205,9 @@ export default class UpdatePublish extends EasCommand {
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish);
|
||||
// Preprocess argv to handle --source-maps with optional value
|
||||
const preprocessedArgv = preprocessSourceMapsArg(this.argv);
|
||||
const { flags: rawFlags } = await this.parse(UpdatePublish, preprocessedArgv);
|
||||
const paginatedQueryOptions = getPaginatedQueryOptions(rawFlags);
|
||||
const {
|
||||
auto: autoFlag,
|
||||
@@ -175,6 +217,8 @@ export default class UpdatePublish extends EasCommand {
|
||||
inputDir,
|
||||
skipBundler,
|
||||
clearCache,
|
||||
noBytecode,
|
||||
sourceMaps,
|
||||
privateKeyPath,
|
||||
json: jsonFlag,
|
||||
nonInteractive,
|
||||
@@ -260,6 +304,8 @@ export default class UpdatePublish extends EasCommand {
|
||||
exp,
|
||||
platformFlag: requestedPlatform,
|
||||
clearCache,
|
||||
noBytecode,
|
||||
sourceMaps,
|
||||
extraEnv: maybeServerEnv,
|
||||
});
|
||||
bundleSpinner.succeed('Exported bundle(s)');
|
||||
@@ -571,6 +617,10 @@ export default class UpdatePublish extends EasCommand {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (isBundleDiffingEnabled(exp)) {
|
||||
await prewarmDiffingAsync(graphqlClient, projectId, newUpdates);
|
||||
}
|
||||
|
||||
if (!skipBundler && emitMetadata) {
|
||||
Log.log('Generating eas-update-metadata.json');
|
||||
await generateEasMetadataAsync(distRoot, getUpdateJsonInfosForUpdates(newUpdates));
|
||||
@@ -747,6 +797,8 @@ export default class UpdatePublish extends EasCommand {
|
||||
inputDir: flags['input-dir'],
|
||||
skipBundler,
|
||||
clearCache: flags['clear-cache'] ? true : !!flags['environment'],
|
||||
noBytecode: flags['no-bytecode'] ?? false,
|
||||
sourceMaps: flags['source-maps'],
|
||||
platform: flags.platform,
|
||||
privateKeyPath: flags['private-key-path'],
|
||||
rolloutPercentage: flags['rollout-percentage'],
|
||||
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
getLimitFlagWithCustomValues,
|
||||
getPaginatedQueryOptions,
|
||||
} from '../../commandUtils/pagination';
|
||||
import { AppPlatform } from '../../graphql/generated';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import {
|
||||
listAndRenderUpdateGroupsOnAppAsync,
|
||||
listAndRenderUpdateGroupsOnBranchAsync,
|
||||
@@ -27,6 +29,14 @@ export default class UpdateList extends EasCommand {
|
||||
exclusive: ['branch'],
|
||||
default: false,
|
||||
}),
|
||||
platform: Flags.enum<RequestedPlatform>({
|
||||
options: Object.values(RequestedPlatform),
|
||||
char: 'p',
|
||||
description: 'Filter updates by platform',
|
||||
}),
|
||||
'runtime-version': Flags.string({
|
||||
description: 'Filter updates by runtime version',
|
||||
}),
|
||||
...EasPaginatedQueryFlags,
|
||||
limit: getLimitFlagWithCustomValues({ defaultTo: 25, limit: 50 }),
|
||||
...EasNonInteractiveAndJsonFlags,
|
||||
@@ -39,7 +49,13 @@ export default class UpdateList extends EasCommand {
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags } = await this.parse(UpdateList);
|
||||
const { branch: branchFlag, all, json: jsonFlag, 'non-interactive': nonInteractive } = flags;
|
||||
const {
|
||||
branch: branchFlag,
|
||||
all,
|
||||
json: jsonFlag,
|
||||
'non-interactive': nonInteractive,
|
||||
platform: requestedPlatform,
|
||||
} = flags;
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -52,9 +68,16 @@ export default class UpdateList extends EasCommand {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
// Build filter object
|
||||
const filter = {
|
||||
platform: toAppPlatform(requestedPlatform),
|
||||
runtimeVersions: flags['runtime-version'] ? [flags['runtime-version']] : undefined,
|
||||
};
|
||||
|
||||
if (all) {
|
||||
await listAndRenderUpdateGroupsOnAppAsync(graphqlClient, {
|
||||
projectId,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
} else {
|
||||
@@ -62,6 +85,7 @@ export default class UpdateList extends EasCommand {
|
||||
await listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName: branchFlag,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
} else {
|
||||
@@ -87,9 +111,20 @@ export default class UpdateList extends EasCommand {
|
||||
await listAndRenderUpdateGroupsOnBranchAsync(graphqlClient, {
|
||||
projectId,
|
||||
branchName: selectedBranch.name,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const toAppPlatform = (requestedPlatform?: RequestedPlatform): AppPlatform | undefined => {
|
||||
if (!requestedPlatform || requestedPlatform === RequestedPlatform.All) {
|
||||
return undefined;
|
||||
} else if (requestedPlatform === RequestedPlatform.Android) {
|
||||
return AppPlatform.Android;
|
||||
} else {
|
||||
return AppPlatform.Ios;
|
||||
}
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
|
||||
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
import Log, { learnMore } from '../../log';
|
||||
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
|
||||
import { UpdatePublishPlatform, getUpdateMessageForCommandAsync } from '../../project/publish';
|
||||
import { getUpdateMessageForCommandAsync } from '../../project/publish';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { scheduleUpdateGroupDeletionAsync } from '../../update/delete';
|
||||
import {
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
republishAsync,
|
||||
} from '../../update/republish';
|
||||
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
|
||||
import { UpdatePublishPlatform } from '../../update/utils';
|
||||
import { CodeSigningInfo, getCodeSigningInfoAsync } from '../../utils/code-signing';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { pollForBackgroundJobReceiptAsync } from '../../utils/pollForBackgroundJobReceiptAsync';
|
||||
|
||||
@@ -9,7 +9,6 @@ import { StatuspageServiceName } from '../../graphql/generated';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { enforceRollBackToEmbeddedUpdateSupportAsync } from '../../project/projectUtils';
|
||||
import {
|
||||
UpdatePublishPlatform,
|
||||
defaultPublishPlatforms,
|
||||
getBranchNameForCommandAsync,
|
||||
getUpdateMessageForCommandAsync,
|
||||
@@ -17,6 +16,7 @@ import {
|
||||
import { ensureEASUpdateIsConfiguredAsync } from '../../update/configure';
|
||||
import { selectRuntimeOnBranchAsync } from '../../update/queries';
|
||||
import { publishRollBackToEmbeddedUpdateAsync } from '../../update/roll-back-to-embedded';
|
||||
import { UpdatePublishPlatform } from '../../update/utils';
|
||||
import { getCodeSigningInfoAsync } from '../../utils/code-signing';
|
||||
import { enableJsonOutput } from '../../utils/json';
|
||||
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
|
||||
|
||||
@@ -44,12 +44,12 @@ import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
import { boolish } from 'getenv';
|
||||
import * as path from 'node:path';
|
||||
import slash from 'slash';
|
||||
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
maybePromptForMissingInputsAsync,
|
||||
@@ -59,34 +59,25 @@ import {
|
||||
} from '../../commandUtils/workflow/inputs';
|
||||
import {
|
||||
fileExistsAsync,
|
||||
infoForActiveWorkflowRunAsync,
|
||||
infoForFailedWorkflowRunAsync,
|
||||
maybeReadStdinAsync,
|
||||
showWorkflowStatusAsync,
|
||||
workflowRunExitCodes,
|
||||
} from '../../commandUtils/workflow/utils';
|
||||
import {
|
||||
WorkflowProjectSourceType,
|
||||
WorkflowRevision,
|
||||
WorkflowRunByIdQuery,
|
||||
WorkflowRunStatus,
|
||||
} from '../../graphql/generated';
|
||||
import { WorkflowRevisionMutation } from '../../graphql/mutations/WorkflowRevisionMutation';
|
||||
import { WorkflowRunMutation } from '../../graphql/mutations/WorkflowRunMutation';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
|
||||
const EXIT_CODES = {
|
||||
WORKFLOW_FAILED: 11,
|
||||
WORKFLOW_CANCELED: 12,
|
||||
WAIT_ABORTED: 13,
|
||||
};
|
||||
|
||||
export default class WorkflowRun extends EasCommand {
|
||||
static override description =
|
||||
'run an EAS workflow. The entire local project directory will be packaged and uploaded to EAS servers for the workflow run, unless the --ref flag is used.';
|
||||
@@ -99,8 +90,7 @@ export default class WorkflowRun extends EasCommand {
|
||||
default: false,
|
||||
allowNo: true,
|
||||
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
|
||||
summary:
|
||||
'Wait for workflow run to complete. Defaults to false. Cannot be used with the --json flag.',
|
||||
summary: 'Wait for workflow run to complete. Defaults to false.',
|
||||
}),
|
||||
input: Flags.string({
|
||||
char: 'F',
|
||||
@@ -129,9 +119,6 @@ export default class WorkflowRun extends EasCommand {
|
||||
const { flags, args } = await this.parse(WorkflowRun);
|
||||
|
||||
if (flags.json) {
|
||||
if (flags.wait) {
|
||||
throw new Error('Cannot use --json and --wait flags simultaneously.');
|
||||
}
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
@@ -148,7 +135,9 @@ export default class WorkflowRun extends EasCommand {
|
||||
const {
|
||||
projectId,
|
||||
exp: { slug: projectName },
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
} = await getDynamicPrivateProjectConfigAsync({
|
||||
skipPlugins: true,
|
||||
});
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
let yamlConfig: string;
|
||||
@@ -370,9 +359,11 @@ export default class WorkflowRun extends EasCommand {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Log.newLine();
|
||||
const { status } = await waitForWorkflowRunToEndAsync(graphqlClient, {
|
||||
const spinnerUsesStdErr = boolish('CI', false) || flags.json;
|
||||
|
||||
const { status } = await showWorkflowStatusAsync(graphqlClient, {
|
||||
workflowRunId,
|
||||
spinnerUsesStdErr,
|
||||
});
|
||||
|
||||
if (flags.json) {
|
||||
@@ -387,71 +378,9 @@ export default class WorkflowRun extends EasCommand {
|
||||
}
|
||||
|
||||
if (status === WorkflowRunStatus.Failure) {
|
||||
process.exit(EXIT_CODES.WORKFLOW_FAILED);
|
||||
process.exit(workflowRunExitCodes.WORKFLOW_FAILED);
|
||||
} else if (status === WorkflowRunStatus.Canceled) {
|
||||
process.exit(EXIT_CODES.WORKFLOW_CANCELED);
|
||||
process.exit(workflowRunExitCodes.WORKFLOW_CANCELED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForWorkflowRunToEndAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{ workflowRunId }: { workflowRunId: string }
|
||||
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
|
||||
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
|
||||
|
||||
const spinner = ora('').start();
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting to start:}`;
|
||||
|
||||
let failedFetchesCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
|
||||
failedFetchesCount = 0;
|
||||
|
||||
switch (workflowRun.status) {
|
||||
case WorkflowRunStatus.New:
|
||||
break;
|
||||
case WorkflowRunStatus.InProgress: {
|
||||
spinner.prefixText = chalk`{bold.green Workflow run is in progress:}`;
|
||||
spinner.text = await infoForActiveWorkflowRunAsync(graphqlClient, workflowRun, 5);
|
||||
break;
|
||||
}
|
||||
case WorkflowRunStatus.ActionRequired:
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting for action:}`;
|
||||
break;
|
||||
|
||||
case WorkflowRunStatus.Canceled:
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow has been canceled.}`;
|
||||
spinner.stopAndPersist();
|
||||
return workflowRun;
|
||||
|
||||
case WorkflowRunStatus.Failure: {
|
||||
spinner.prefixText = chalk`{bold.red Workflow has failed.}`;
|
||||
const failedInfo = await infoForFailedWorkflowRunAsync(graphqlClient, workflowRun);
|
||||
spinner.fail(failedInfo);
|
||||
return workflowRun;
|
||||
}
|
||||
case WorkflowRunStatus.Success:
|
||||
spinner.prefixText = chalk`{bold.green Workflow has completed successfully.}`;
|
||||
spinner.succeed('');
|
||||
return workflowRun;
|
||||
}
|
||||
} catch {
|
||||
spinner.text = '⚠ Failed to fetch the workflow run status. Check your network connection.';
|
||||
|
||||
failedFetchesCount += 1;
|
||||
|
||||
if (failedFetchesCount > 6) {
|
||||
spinner.fail('Failed to fetch the workflow run status 6 times in a row. Aborting wait.');
|
||||
process.exit(EXIT_CODES.WAIT_ABORTED);
|
||||
}
|
||||
}
|
||||
|
||||
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* EAS Workflow Status Command
|
||||
*
|
||||
* This command shows the status of an existing workflow run.
|
||||
*
|
||||
* If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.
|
||||
*
|
||||
* If the selected run is still in progress, the command will show the progress of the run, with an option
|
||||
* to show periodic status updates while waiting for completion (similar to `eas workflow:run --wait`).
|
||||
*
|
||||
*/
|
||||
|
||||
import { Flags } from '@oclif/core';
|
||||
import { boolish } from 'getenv';
|
||||
|
||||
import { getWorkflowRunUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASNonInteractiveFlag, EasJsonOnlyFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
choiceFromWorkflowRun,
|
||||
processWorkflowRuns,
|
||||
showWorkflowStatusAsync,
|
||||
workflowRunExitCodes,
|
||||
} from '../../commandUtils/workflow/utils';
|
||||
import { WorkflowRunStatus } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log, { link } from '../../log';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
|
||||
export default class WorkflowStatus extends EasCommand {
|
||||
static override description =
|
||||
'show the status of an existing workflow run. If no run ID is provided, you will be prompted to select from recent workflow runs for the current project.';
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'WORKFLOW_RUN_ID',
|
||||
description: 'A workflow run ID.',
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
...EASNonInteractiveFlag,
|
||||
wait: Flags.boolean({
|
||||
default: false,
|
||||
allowNo: true,
|
||||
description: 'Exit codes: 0 = success, 11 = failure, 12 = canceled, 13 = wait aborted.',
|
||||
summary: 'Wait for workflow run to complete. Defaults to false.',
|
||||
}),
|
||||
...EasJsonOnlyFlag,
|
||||
};
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.DynamicProjectConfig,
|
||||
...this.ContextOptions.ProjectDir,
|
||||
...this.ContextOptions.Vcs,
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { flags, args } = await this.parse(WorkflowStatus);
|
||||
|
||||
if (flags.json) {
|
||||
enableJsonOutput();
|
||||
}
|
||||
|
||||
const {
|
||||
getDynamicPrivateProjectConfigAsync,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(WorkflowStatus, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: null,
|
||||
});
|
||||
|
||||
const {
|
||||
projectId,
|
||||
exp: { slug: projectName },
|
||||
} = await getDynamicPrivateProjectConfigAsync();
|
||||
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
|
||||
|
||||
let workflowRunId = args.WORKFLOW_RUN_ID;
|
||||
|
||||
if (!workflowRunId && flags['non-interactive']) {
|
||||
throw new Error('Workflow run ID is required in non-interactive mode');
|
||||
}
|
||||
if (!workflowRunId) {
|
||||
const queryResult = await AppQuery.byIdWorkflowRunsFilteredByStatusAsync(
|
||||
graphqlClient,
|
||||
projectId,
|
||||
undefined,
|
||||
50
|
||||
);
|
||||
const runs = processWorkflowRuns(queryResult);
|
||||
if (runs.length === 0) {
|
||||
Log.warn('No workflow runs to show');
|
||||
return;
|
||||
}
|
||||
const answers = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'selectedRun',
|
||||
message: 'Select a workflow run:',
|
||||
choices: runs.map(run => choiceFromWorkflowRun(run)),
|
||||
});
|
||||
workflowRunId = answers.selectedRun;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log(`See logs: ${link(getWorkflowRunUrl(account.name, projectName, workflowRunId))}`);
|
||||
Log.addNewLineIfNone();
|
||||
|
||||
const spinnerUsesStdErr = boolish('CI', false) || flags.json;
|
||||
|
||||
await showWorkflowStatusAsync(graphqlClient, {
|
||||
workflowRunId,
|
||||
spinnerUsesStdErr,
|
||||
waitForCompletion: flags.wait,
|
||||
});
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
const status = workflowRun.status;
|
||||
|
||||
if (flags.json) {
|
||||
printJsonOnlyOutput({
|
||||
...workflowRun,
|
||||
url: getWorkflowRunUrl(account.name, projectName, workflowRunId),
|
||||
});
|
||||
}
|
||||
|
||||
if (status === WorkflowRunStatus.Failure) {
|
||||
process.exit(workflowRunExitCodes.WORKFLOW_FAILED);
|
||||
} else if (status === WorkflowRunStatus.Canceled) {
|
||||
process.exit(workflowRunExitCodes.WORKFLOW_CANCELED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export class SetUpTargetBuildCredentials {
|
||||
constructor(private readonly options: Options) {}
|
||||
|
||||
async runAsync(ctx: CredentialsContext): Promise<IosAppBuildCredentialsFragment> {
|
||||
const { app, entitlements } = this.options;
|
||||
const { app, entitlements, target } = this.options;
|
||||
|
||||
await ctx.bestEffortAppStoreAuthenticateAsync();
|
||||
|
||||
@@ -35,7 +35,11 @@ export class SetUpTargetBuildCredentials {
|
||||
bundleIdentifier: app.bundleIdentifier,
|
||||
projectName: app.projectName,
|
||||
},
|
||||
{ entitlements, usesBroadcastPushNotifications: ctx.usesBroadcastPushNotifications }
|
||||
{
|
||||
entitlements,
|
||||
usesBroadcastPushNotifications: ctx.usesBroadcastPushNotifications,
|
||||
parentBundleIdentifier: target.parentBundleIdentifier,
|
||||
}
|
||||
);
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -11,9 +11,22 @@ import { assertContractMessagesAsync } from './contractMessages';
|
||||
import Log from '../../../log';
|
||||
import { ora } from '../../../ora';
|
||||
|
||||
/**
|
||||
* The entitlement key that indicates a target is an App Clip.
|
||||
* When present, the target requires special bundle ID registration with a parent app relationship.
|
||||
*/
|
||||
const APP_CLIP_PARENT_APPLICATION_IDENTIFIERS_ENTITLEMENT =
|
||||
'com.apple.developer.parent-application-identifiers';
|
||||
|
||||
export interface IosCapabilitiesOptions {
|
||||
entitlements: JSONObject;
|
||||
usesBroadcastPushNotifications: boolean;
|
||||
/**
|
||||
* The bundle identifier of the parent app. Required for App Clips.
|
||||
* App Clips are detected by the presence of the
|
||||
* `com.apple.developer.parent-application-identifiers` entitlement.
|
||||
*/
|
||||
parentBundleIdentifier?: string;
|
||||
}
|
||||
|
||||
export interface AppLookupParams {
|
||||
@@ -43,7 +56,13 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
options?: IosCapabilitiesOptions
|
||||
): Promise<void> {
|
||||
const context = getRequestContext(authCtx);
|
||||
const spinner = ora(`Linking bundle identifier ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
// Detect App Clips by the presence of the parent-application-identifiers entitlement
|
||||
const isAppClip = Boolean(
|
||||
options?.entitlements?.[APP_CLIP_PARENT_APPLICATION_IDENTIFIERS_ENTITLEMENT]
|
||||
);
|
||||
const typeLabel = isAppClip ? 'App Clip bundle identifier' : 'bundle identifier';
|
||||
const spinner = ora(`Linking ${typeLabel} ${chalk.dim(bundleIdentifier)}`).start();
|
||||
|
||||
let bundleId: BundleId | null;
|
||||
try {
|
||||
@@ -51,23 +70,37 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
bundleId = await BundleId.findAsync(context, { identifier: bundleIdentifier });
|
||||
|
||||
if (!bundleId) {
|
||||
spinner.text = `Registering bundle identifier ${chalk.dim(bundleIdentifier)}`;
|
||||
// If it doesn't exist, create it
|
||||
bundleId = await BundleId.createAsync(context, {
|
||||
name,
|
||||
identifier: bundleIdentifier,
|
||||
});
|
||||
spinner.text = `Registering ${typeLabel} ${chalk.dim(bundleIdentifier)}`;
|
||||
|
||||
if (isAppClip && options?.parentBundleIdentifier) {
|
||||
// App Clips require the parent bundle ID's opaque ID
|
||||
bundleId = await createAppClipBundleIdAsync(context, {
|
||||
name,
|
||||
bundleIdentifier,
|
||||
parentBundleIdentifier: options.parentBundleIdentifier,
|
||||
});
|
||||
} else {
|
||||
// If it doesn't exist, create it
|
||||
bundleId = await BundleId.createAsync(context, {
|
||||
name,
|
||||
identifier: bundleIdentifier,
|
||||
});
|
||||
}
|
||||
}
|
||||
spinner.succeed(`Bundle identifier registered ${chalk.dim(bundleIdentifier)}`);
|
||||
spinner.succeed(
|
||||
`${isAppClip ? 'App Clip bundle' : 'Bundle'} identifier registered ${chalk.dim(
|
||||
bundleIdentifier
|
||||
)}`
|
||||
);
|
||||
} catch (err: any) {
|
||||
if (err.message.match(/An App ID with Identifier '(.*)' is not available/)) {
|
||||
spinner.fail(
|
||||
`The bundle identifier ${chalk.bold(bundleIdentifier)} is not available to team "${
|
||||
`The ${typeLabel} ${chalk.bold(bundleIdentifier)} is not available to team "${
|
||||
authCtx.team.name
|
||||
}" (${authCtx.team.id}), change it in your app config and try again.`
|
||||
);
|
||||
} else {
|
||||
spinner.fail(`Failed to register bundle identifier ${chalk.dim(bundleIdentifier)}`);
|
||||
spinner.fail(`Failed to register ${typeLabel} ${chalk.dim(bundleIdentifier)}`);
|
||||
|
||||
// Assert contract errors for easier resolution when the user has an expired developer account.
|
||||
if (
|
||||
@@ -93,6 +126,42 @@ export async function ensureBundleIdExistsWithNameAsync(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an App Clip bundle identifier.
|
||||
* App Clips require the parent bundle ID's opaque ID for registration.
|
||||
*/
|
||||
async function createAppClipBundleIdAsync(
|
||||
context: RequestContext,
|
||||
{
|
||||
name,
|
||||
bundleIdentifier,
|
||||
parentBundleIdentifier,
|
||||
}: {
|
||||
name: string;
|
||||
bundleIdentifier: string;
|
||||
parentBundleIdentifier: string;
|
||||
}
|
||||
): Promise<BundleId> {
|
||||
// First, find the parent bundle ID to get its opaque ID
|
||||
const parentBundleId = await BundleId.findAsync(context, {
|
||||
identifier: parentBundleIdentifier,
|
||||
});
|
||||
|
||||
if (!parentBundleId) {
|
||||
throw new Error(
|
||||
`Cannot create App Clip bundle identifier "${bundleIdentifier}" because the parent bundle identifier "${parentBundleIdentifier}" is not registered. ` +
|
||||
`Please ensure the parent app's bundle identifier is registered first.`
|
||||
);
|
||||
}
|
||||
|
||||
// Create the App Clip bundle ID with the parent relationship
|
||||
return await BundleId.createAppClipAsync(context, {
|
||||
name,
|
||||
identifier: bundleIdentifier,
|
||||
parentBundleIdId: parentBundleId.id,
|
||||
});
|
||||
}
|
||||
|
||||
export async function syncCapabilitiesAsync(
|
||||
bundleId: BundleId,
|
||||
{ entitlements, ...rest }: IosCapabilitiesOptions
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ManageIos } from './ManageIos';
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { DynamicConfigContextFn } from '../../commandUtils/context/DynamicProjectConfigContextField';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { selectPlatformAsync } from '../../platform';
|
||||
import { selectPlatformWithExitOptionAsync } from '../../platform';
|
||||
import { Actor } from '../../user/User';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
import { CredentialsContextProjectInfo } from '../context';
|
||||
@@ -20,7 +20,7 @@ export class SelectPlatform {
|
||||
) {}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const platform = await selectPlatformAsync(this.flagPlatform);
|
||||
const platform = await selectPlatformWithExitOptionAsync(this.flagPlatform);
|
||||
|
||||
if (platform === 'ios') {
|
||||
await new ManageIos(this, process.cwd()).runAsync();
|
||||
|
||||
+567
-109
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,54 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AccountUsageForOverageWarningQuery,
|
||||
AccountUsageForOverageWarningQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const AccountUsageQuery = {
|
||||
async getUsageForOverageWarningAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
accountId: string,
|
||||
currentDate: Date
|
||||
): Promise<AccountUsageForOverageWarningQuery['account']['byId']> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<AccountUsageForOverageWarningQuery, AccountUsageForOverageWarningQueryVariables>(
|
||||
gql`
|
||||
query AccountUsageForOverageWarning($accountId: String!, $currentDate: DateTime!) {
|
||||
account {
|
||||
byId(accountId: $accountId) {
|
||||
id
|
||||
name
|
||||
subscription {
|
||||
id
|
||||
name
|
||||
}
|
||||
usageMetrics {
|
||||
EAS_BUILD: byBillingPeriod(date: $currentDate, service: BUILDS) {
|
||||
id
|
||||
planMetrics {
|
||||
id
|
||||
serviceMetric
|
||||
value
|
||||
limit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ accountId, currentDate: currentDate.toISOString() },
|
||||
{
|
||||
additionalTypenames: ['Account', 'AccountUsageMetrics', 'UsageMetricTotal'],
|
||||
}
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.account.byId;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
AssetSignedUrlResult,
|
||||
GetAssetSignedUrlsQuery,
|
||||
GetAssetSignedUrlsQueryVariables,
|
||||
} from '../generated';
|
||||
|
||||
export const AssetQuery = {
|
||||
async getSignedUrlsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
updateId: string,
|
||||
storageKeys: string[]
|
||||
): Promise<AssetSignedUrlResult[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<GetAssetSignedUrlsQuery, GetAssetSignedUrlsQueryVariables>(
|
||||
gql`
|
||||
query GetAssetSignedUrls($updateId: ID!, $storageKeys: [String!]!) {
|
||||
asset {
|
||||
signedUrls(updateId: $updateId, storageKeys: $storageKeys) {
|
||||
storageKey
|
||||
url
|
||||
headers
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
updateId,
|
||||
storageKeys,
|
||||
},
|
||||
{ additionalTypenames: [] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
return data.asset.signedUrls;
|
||||
},
|
||||
};
|
||||
@@ -63,15 +63,24 @@ export const BranchQuery = {
|
||||
}
|
||||
return updateBranchByName;
|
||||
},
|
||||
async getLatestUpdateIdOnBranchAsync(
|
||||
async getUpdateIdsOnBranchAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
platform,
|
||||
runtimeVersion,
|
||||
}: { appId: string; branchName: string; platform: AppPlatform; runtimeVersion: string }
|
||||
): Promise<string | null> {
|
||||
offset = 0,
|
||||
limit = 1,
|
||||
}: {
|
||||
appId: string;
|
||||
branchName: string;
|
||||
platform: AppPlatform;
|
||||
runtimeVersion: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
): Promise<string[]> {
|
||||
const response = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query<ViewLatestUpdateOnBranchQuery, ViewLatestUpdateOnBranchQueryVariables>(
|
||||
@@ -81,6 +90,8 @@ export const BranchQuery = {
|
||||
$branchName: String!
|
||||
$platform: AppPlatform!
|
||||
$runtimeVersion: String!
|
||||
$offset: Int!
|
||||
$limit: Int!
|
||||
) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
@@ -88,8 +99,8 @@ export const BranchQuery = {
|
||||
updateBranchByName(name: $branchName) {
|
||||
id
|
||||
updates(
|
||||
offset: 0
|
||||
limit: 1
|
||||
offset: $offset
|
||||
limit: $limit
|
||||
filter: { platform: $platform, runtimeVersions: [$runtimeVersion] }
|
||||
) {
|
||||
id
|
||||
@@ -104,6 +115,8 @@ export const BranchQuery = {
|
||||
branchName,
|
||||
platform,
|
||||
runtimeVersion,
|
||||
offset,
|
||||
limit,
|
||||
},
|
||||
{ additionalTypenames: ['UpdateBranch'] }
|
||||
)
|
||||
@@ -113,11 +126,26 @@ export const BranchQuery = {
|
||||
if (!updateBranchByName) {
|
||||
throw new BranchNotFoundError(`Could not find a branch named "${branchName}".`);
|
||||
}
|
||||
const latestUpdate = updateBranchByName.updates[0];
|
||||
if (!latestUpdate) {
|
||||
return null;
|
||||
}
|
||||
return latestUpdate.id;
|
||||
return updateBranchByName.updates.map(update => update.id);
|
||||
},
|
||||
async getLatestUpdateIdOnBranchAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
appId,
|
||||
branchName,
|
||||
platform,
|
||||
runtimeVersion,
|
||||
}: { appId: string; branchName: string; platform: AppPlatform; runtimeVersion: string }
|
||||
): Promise<string | null> {
|
||||
const updateIds = await this.getUpdateIdsOnBranchAsync(graphqlClient, {
|
||||
appId,
|
||||
branchName,
|
||||
platform,
|
||||
runtimeVersion,
|
||||
offset: 0,
|
||||
limit: 1,
|
||||
});
|
||||
return updateIds[0] ?? null;
|
||||
},
|
||||
async listBranchesOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
|
||||
@@ -12,12 +12,6 @@ jest.mock('../../config/writer');
|
||||
|
||||
describe(AgeRatingTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AgeRatingTask().prepareAsync({ context: {} as any });
|
||||
|
||||
await expect(promise).rejects.toThrow('not prepared');
|
||||
});
|
||||
|
||||
it('loads age rating from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/ageRatingDeclaration`)
|
||||
|
||||
@@ -35,14 +35,16 @@ describe(AppInfoTask, () => {
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('aborts when app info is not loaded', async () => {
|
||||
it('skips when app info is not loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
const promise = new AppInfoTask().downloadAsync({
|
||||
|
||||
await new AppInfoTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { info: undefined, infoLocales: [] } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('info not initialized');
|
||||
// Should not call any setter methods when info is not available
|
||||
expect(writer.setCategories).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sets categories when info is loaded', async () => {
|
||||
|
||||
@@ -11,12 +11,6 @@ jest.mock('../../config/writer');
|
||||
|
||||
describe(AppReviewDetailTask, () => {
|
||||
describe('prepareAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppReviewDetailTask().prepareAsync({ context: {} as any });
|
||||
|
||||
await expect(promise).rejects.toThrow('version not init');
|
||||
});
|
||||
|
||||
it('loads review details from app version', async () => {
|
||||
const scope = nock('https://api.appstoreconnect.apple.com')
|
||||
.get(`/v1/${AppStoreVersion.type}/stub-id/appStoreReviewDetail`)
|
||||
|
||||
@@ -254,13 +254,16 @@ describe(AppVersionTask, () => {
|
||||
});
|
||||
|
||||
describe('downloadAsync', () => {
|
||||
it('aborts when version is not loaded', async () => {
|
||||
const promise = new AppVersionTask().downloadAsync({
|
||||
config: new AppleConfigWriter(),
|
||||
it('skips when version is not loaded', async () => {
|
||||
const writer = jest.mocked(new AppleConfigWriter());
|
||||
|
||||
await new AppVersionTask().downloadAsync({
|
||||
config: writer,
|
||||
context: { version: undefined } as any,
|
||||
});
|
||||
|
||||
await expect(promise).rejects.toThrow('not initialized');
|
||||
// Should not call any setter methods when version is not available
|
||||
expect(writer.setVersion).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('sets version when loaded', async () => {
|
||||
|
||||
@@ -15,8 +15,7 @@ export class AgeRatingTask extends AppleTask {
|
||||
public name = (): string => 'age rating declarations';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
assert(context.version, `App version information is not prepared, can't update age rating`);
|
||||
context.ageRating = (await context.version.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
context.ageRating = (await context.version!.getAgeRatingDeclarationAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
|
||||
@@ -18,19 +18,32 @@ export class AppInfoTask extends AppleTask {
|
||||
public name = (): string => 'app information';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
const info = await retryIfNullAsync(() => context.app.getEditAppInfoAsync());
|
||||
assert(info, 'Could not resolve the editable app info to update');
|
||||
let info = await retryIfNullAsync(() => context.app.getEditAppInfoAsync());
|
||||
|
||||
// Fallback to live app info if no editable info is found (for metadata:pull)
|
||||
if (!info) {
|
||||
info = await context.app.getLiveAppInfoAsync();
|
||||
}
|
||||
|
||||
if (!info) {
|
||||
// No app info available - this is OK for metadata:pull
|
||||
// when the app hasn't been set up yet
|
||||
return;
|
||||
}
|
||||
|
||||
context.info = info;
|
||||
context.infoLocales = await info.getLocalizationsAsync();
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
assert(context.info, `App info not initialized, can't download info`);
|
||||
if (!context.info) {
|
||||
// No app info available to download - skip silently
|
||||
return;
|
||||
}
|
||||
|
||||
config.setCategories(context.info.attributes);
|
||||
|
||||
for (const locale of context.infoLocales) {
|
||||
for (const locale of context.infoLocales ?? []) {
|
||||
config.setInfoLocale(locale.attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,7 @@ export class AppReviewDetailTask extends AppleTask {
|
||||
public name = (): string => 'app review detail';
|
||||
|
||||
public async prepareAsync({ context }: TaskPrepareOptions): Promise<void> {
|
||||
assert(context.version, `App version not initialized, can't download store review details`);
|
||||
context.reviewDetail = (await context.version.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
context.reviewDetail = (await context.version!.getAppStoreReviewDetailAsync()) ?? undefined;
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
|
||||
@@ -57,7 +57,11 @@ export class AppVersionTask extends AppleTask {
|
||||
this.options
|
||||
);
|
||||
|
||||
assert(version, 'Could not resolve a live or editable app version');
|
||||
if (!version) {
|
||||
// No live or editable version available - this is OK for metadata:pull
|
||||
// when the app hasn't been submitted yet
|
||||
return;
|
||||
}
|
||||
|
||||
context.version = version;
|
||||
context.versionIsFirst = versionIsFirst;
|
||||
@@ -67,13 +71,16 @@ export class AppVersionTask extends AppleTask {
|
||||
}
|
||||
|
||||
public async downloadAsync({ config, context }: TaskDownloadOptions): Promise<void> {
|
||||
assert(context.version, `App version not initialized, can't download version`);
|
||||
if (!context.version) {
|
||||
// No version available to download - skip silently
|
||||
return;
|
||||
}
|
||||
|
||||
config.setVersion(context.version.attributes);
|
||||
config.setVersionReleaseType(context.version.attributes);
|
||||
config.setVersionReleasePhased(context.versionPhasedRelease?.attributes);
|
||||
|
||||
for (const locale of context.versionLocales) {
|
||||
for (const locale of context.versionLocales ?? []) {
|
||||
config.setVersionLocale(locale.attributes);
|
||||
}
|
||||
}
|
||||
@@ -211,6 +218,12 @@ async function resolveVersionAsync(
|
||||
version = await retryIfNullAsync(() => app.getEditAppStoreVersionAsync({ platform }));
|
||||
}
|
||||
|
||||
// Fallback to live version if no editable version is found (for metadata:pull)
|
||||
if (!version) {
|
||||
version = await app.getLiveAppStoreVersionAsync({ platform });
|
||||
versionIsLive = !!version;
|
||||
}
|
||||
|
||||
const versions = await app.getAppStoreVersionsAsync({
|
||||
query: { limit: 2, filter: { platform } },
|
||||
});
|
||||
|
||||
@@ -9,11 +9,13 @@ export async function runGitCloneAsync({
|
||||
githubRepositoryName,
|
||||
githubUsername,
|
||||
cloneMethod,
|
||||
showOutput = true,
|
||||
}: {
|
||||
githubUsername: string;
|
||||
githubRepositoryName: string;
|
||||
targetProjectDir: string;
|
||||
cloneMethod: 'ssh' | 'https';
|
||||
showOutput?: boolean;
|
||||
}): Promise<{
|
||||
targetProjectDir: string;
|
||||
}> {
|
||||
@@ -29,6 +31,7 @@ export async function runGitCloneAsync({
|
||||
return line.includes('Cloning into');
|
||||
},
|
||||
showSpinner: false,
|
||||
showOutput,
|
||||
});
|
||||
return { targetProjectDir };
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -35,5 +35,6 @@ export async function installDependenciesAsync({
|
||||
);
|
||||
},
|
||||
showOutput: outputLevel !== 'none',
|
||||
showSpinner: outputLevel !== 'none',
|
||||
});
|
||||
}
|
||||
@@ -21,7 +21,9 @@ export async function runCommandAsync({
|
||||
showSpinner?: boolean;
|
||||
showOutput?: boolean;
|
||||
}): Promise<void> {
|
||||
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
|
||||
if (showOutput) {
|
||||
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
|
||||
}
|
||||
let spinner: Ora | undefined;
|
||||
if (showSpinner) {
|
||||
spinner = ora(`${chalk.bold(`${command} ${args.join(' ')}`)}`).start();
|
||||
@@ -71,6 +73,10 @@ export async function runCommandAsync({
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!showOutput) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showSpinner) {
|
||||
spinner?.succeed(`${chalk.bold(`${command} ${args.join(' ')}`)} succeeded`);
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Platform } from '@expo/eas-build-job';
|
||||
|
||||
import { AppPlatform } from './graphql/generated';
|
||||
import Log from './log';
|
||||
import { promptAsync } from './prompts';
|
||||
|
||||
export const appPlatformDisplayNames: Record<AppPlatform, string> = {
|
||||
@@ -47,21 +48,42 @@ export async function selectRequestedPlatformAsync(platform?: string): Promise<R
|
||||
return requestedPlatform;
|
||||
}
|
||||
|
||||
export async function selectPlatformWithExitOptionAsync(platform?: string): Promise<Platform> {
|
||||
return await selectPlatformInternalAsync(platform, true);
|
||||
}
|
||||
export async function selectPlatformAsync(platform?: string): Promise<Platform> {
|
||||
return await selectPlatformInternalAsync(platform, false);
|
||||
}
|
||||
|
||||
async function selectPlatformInternalAsync(
|
||||
platform?: string,
|
||||
allowExit?: boolean
|
||||
): Promise<Platform> {
|
||||
if (platform && Object.values(Platform).includes(platform.toLowerCase() as Platform)) {
|
||||
return platform.toLowerCase() as Platform;
|
||||
}
|
||||
|
||||
const { resolvedPlatform } = await promptAsync({
|
||||
const platformChoices: { title: string; value: Platform | 'Exit' }[] = [
|
||||
{ title: 'Android', value: Platform.ANDROID },
|
||||
{ title: 'iOS', value: Platform.IOS },
|
||||
];
|
||||
|
||||
if (allowExit) {
|
||||
platformChoices.push({ title: 'Exit', value: 'Exit' });
|
||||
}
|
||||
|
||||
const result: any = await promptAsync({
|
||||
type: 'select',
|
||||
message: 'Select platform',
|
||||
name: 'resolvedPlatform',
|
||||
choices: [
|
||||
{ title: 'Android', value: Platform.ANDROID },
|
||||
{ title: 'iOS', value: Platform.IOS },
|
||||
],
|
||||
choices: platformChoices,
|
||||
});
|
||||
return resolvedPlatform;
|
||||
if (result.resolvedPlatform === 'Exit') {
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Exiting');
|
||||
process.exit(0);
|
||||
}
|
||||
return result.resolvedPlatform;
|
||||
}
|
||||
|
||||
export function toPlatforms(requestedPlatform: RequestedPlatform): Platform[] {
|
||||
|
||||
@@ -9,9 +9,11 @@ import { AssetMetadataStatus } from '../../graphql/generated';
|
||||
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
|
||||
import { PublishQuery } from '../../graphql/queries/PublishQuery';
|
||||
import { RequestedPlatform } from '../../platform';
|
||||
import { expoCommandAsync } from '../../utils/expoCli';
|
||||
import {
|
||||
MetadataJoi,
|
||||
RawAsset,
|
||||
buildBundlesAsync,
|
||||
buildUnsortedUpdateInfoGroupAsync,
|
||||
collectAssetsAsync,
|
||||
convertAssetToUpdateInfoGroupFormatAsync,
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
getAssetHashFromPath,
|
||||
getBase64URLEncoding,
|
||||
getOriginalPathFromAssetMap,
|
||||
getSourceMapExportCommandArgs,
|
||||
getStorageKey,
|
||||
getStorageKeyForAssetAsync,
|
||||
guessContentTypeFromExtension,
|
||||
@@ -30,6 +33,11 @@ import {
|
||||
|
||||
jest.mock('../../uploads');
|
||||
jest.mock('fs');
|
||||
jest.mock('../../utils/expoCli', () => ({
|
||||
expoCommandAsync: jest.fn(),
|
||||
shouldUseVersionedExpoCLI: jest.fn(() => true),
|
||||
shouldUseVersionedExpoCLIWithExplicitPlatforms: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
const dummyFileBuffer = Buffer.from('dummy-file');
|
||||
|
||||
@@ -702,3 +710,142 @@ describe(uploadAssetsAsync, () => {
|
||||
expect(onAssetUploadResultsChangedFn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe(getSourceMapExportCommandArgs, () => {
|
||||
it('returns --dump-sourcemap when sourceMaps is undefined, empty array when "false"', () => {
|
||||
expect(getSourceMapExportCommandArgs({ sourceMaps: undefined, sdkVersion: '50.0.0' })).toEqual([
|
||||
'--dump-sourcemap',
|
||||
]);
|
||||
expect(getSourceMapExportCommandArgs({ sourceMaps: 'false', sdkVersion: '55.0.0' })).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns --source-maps with value only for SDK >= 55', () => {
|
||||
// SDK < 55: no value
|
||||
expect(getSourceMapExportCommandArgs({ sourceMaps: 'inline', sdkVersion: '50.0.0' })).toEqual([
|
||||
'--source-maps',
|
||||
]);
|
||||
// SDK >= 55: includes value
|
||||
expect(getSourceMapExportCommandArgs({ sourceMaps: 'inline', sdkVersion: '55.0.0' })).toEqual([
|
||||
'--source-maps',
|
||||
'inline',
|
||||
]);
|
||||
// "true" never includes a value
|
||||
expect(getSourceMapExportCommandArgs({ sourceMaps: 'true', sdkVersion: '55.0.0' })).toEqual([
|
||||
'--source-maps',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(buildBundlesAsync, () => {
|
||||
const projectDir = '/test-project';
|
||||
const inputDir = 'dist';
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(projectDir, 'package.json'),
|
||||
JSON.stringify({ name: 'test', version: '1.0.0' })
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.remove(projectDir);
|
||||
});
|
||||
|
||||
it('uses --source-maps instead of --dump-sourcemap when provided', async () => {
|
||||
// SDK < 55: --source-maps is a boolean flag, value not supported
|
||||
// When sourceMaps is 'inline' but SDK < 55, pass --source-maps without the value
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '50.0.0' },
|
||||
platformFlag: 'all',
|
||||
sourceMaps: 'inline',
|
||||
});
|
||||
let args = jest.mocked(expoCommandAsync).mock.calls[0][1];
|
||||
expect(args).toContain('--source-maps');
|
||||
expect(args).not.toContain('inline');
|
||||
expect(args).not.toContain('--dump-sourcemap');
|
||||
|
||||
// SDK >= 55: --source-maps supports a value (e.g., 'inline')
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '55.0.0' },
|
||||
platformFlag: 'all',
|
||||
sourceMaps: 'inline',
|
||||
});
|
||||
expect(expoCommandAsync).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
expect.arrayContaining(['--source-maps', 'inline']),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).not.toContain('--dump-sourcemap');
|
||||
|
||||
// When sourceMaps is 'true', pass --source-maps without a value (regardless of SDK version)
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '55.0.0' },
|
||||
platformFlag: 'all',
|
||||
sourceMaps: 'true',
|
||||
});
|
||||
args = jest.mocked(expoCommandAsync).mock.calls[0][1];
|
||||
expect(args).toContain('--source-maps');
|
||||
expect(args).not.toContain('true');
|
||||
expect(args).not.toContain('--dump-sourcemap');
|
||||
|
||||
// When sourceMaps is "false", don't pass any source map flags
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '50.0.0' },
|
||||
platformFlag: 'all',
|
||||
sourceMaps: 'false',
|
||||
});
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).not.toContain('--source-maps');
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).not.toContain('--dump-sourcemap');
|
||||
|
||||
// When sourceMaps is undefined, fall back to --dump-sourcemap
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '50.0.0' },
|
||||
platformFlag: 'all',
|
||||
});
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).not.toContain('--source-maps');
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).toContain('--dump-sourcemap');
|
||||
});
|
||||
|
||||
it('passes --no-bytecode only when true', async () => {
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '50.0.0' },
|
||||
platformFlag: 'all',
|
||||
noBytecode: true,
|
||||
});
|
||||
expect(expoCommandAsync).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
expect.arrayContaining(['--no-bytecode']),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
jest.mocked(expoCommandAsync).mockClear();
|
||||
await buildBundlesAsync({
|
||||
projectDir,
|
||||
inputDir,
|
||||
exp: { sdkVersion: '50.0.0' },
|
||||
platformFlag: 'all',
|
||||
noBytecode: false,
|
||||
});
|
||||
expect(jest.mocked(expoCommandAsync).mock.calls[0][1]).not.toContain('--no-bytecode');
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import mime from 'mime';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import semver from 'semver';
|
||||
|
||||
import { maybeUploadFingerprintAsync } from './maybeUploadFingerprintAsync';
|
||||
import { isModernExpoUpdatesCLIWithRuntimeVersionCommandSupportedAsync } from './projectUtils';
|
||||
@@ -22,7 +23,6 @@ import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGr
|
||||
import { PaginatedQueryOptions } from '../commandUtils/pagination';
|
||||
import { FingerprintOptions, createFingerprintsByKeyAsync } from '../fingerprint/cli';
|
||||
import {
|
||||
AppPlatform,
|
||||
AssetMetadataStatus,
|
||||
BuildFragment,
|
||||
FingerprintSourceInput,
|
||||
@@ -39,8 +39,10 @@ import { promptAsync } from '../prompts';
|
||||
import { getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync } from '../update/getBranchFromChannelNameAndCreateAndLinkIfNotExistsAsync';
|
||||
import {
|
||||
UpdateJsonInfo,
|
||||
UpdatePublishPlatform,
|
||||
formatUpdateMessage,
|
||||
truncateString as truncateUpdateMessage,
|
||||
updatePublishPlatformToAppPlatform,
|
||||
} from '../update/utils';
|
||||
import { PresignedPost, uploadWithPresignedPostWithRetryAsync } from '../uploads';
|
||||
import {
|
||||
@@ -56,9 +58,6 @@ import mapMapAsync from '../utils/expodash/mapMapAsync';
|
||||
import uniqBy from '../utils/expodash/uniqBy';
|
||||
import { Client } from '../vcs/vcs';
|
||||
|
||||
// update publish does not currently support web
|
||||
export type UpdatePublishPlatform = 'ios' | 'android';
|
||||
|
||||
type Metadata = {
|
||||
version: number;
|
||||
bundler: 'metro';
|
||||
@@ -210,6 +209,8 @@ export async function buildBundlesAsync({
|
||||
exp,
|
||||
platformFlag,
|
||||
clearCache,
|
||||
noBytecode,
|
||||
sourceMaps,
|
||||
extraEnv,
|
||||
}: {
|
||||
projectDir: string;
|
||||
@@ -217,6 +218,8 @@ export async function buildBundlesAsync({
|
||||
exp: Pick<ExpoConfig, 'sdkVersion' | 'web'>;
|
||||
platformFlag: ExpoCLIExportPlatformFlag;
|
||||
clearCache?: boolean;
|
||||
noBytecode?: boolean;
|
||||
sourceMaps?: string;
|
||||
extraEnv?: Record<string, string | undefined> | undefined;
|
||||
}): Promise<void> {
|
||||
const packageJSON = JsonFile.read(path.resolve(projectDir, 'package.json'));
|
||||
@@ -254,16 +257,19 @@ export async function buildBundlesAsync({
|
||||
? ['--platform', 'ios', '--platform', 'android']
|
||||
: ['--platform', platformFlag];
|
||||
|
||||
const sourceMapArgs = getSourceMapExportCommandArgs({ sourceMaps, sdkVersion: exp.sdkVersion });
|
||||
|
||||
await expoCommandAsync(
|
||||
projectDir,
|
||||
[
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
...sourceMapArgs,
|
||||
'--dump-assetmap',
|
||||
...platformArgs,
|
||||
...(clearCache ? ['--clear'] : []),
|
||||
...(noBytecode ? ['--no-bytecode'] : []),
|
||||
],
|
||||
{
|
||||
extraEnv,
|
||||
@@ -282,16 +288,19 @@ export async function buildBundlesAsync({
|
||||
);
|
||||
}
|
||||
|
||||
const sourceMapArgs = getSourceMapExportCommandArgs({ sourceMaps, sdkVersion: exp.sdkVersion });
|
||||
|
||||
await expoCommandAsync(
|
||||
projectDir,
|
||||
[
|
||||
'export',
|
||||
'--output-dir',
|
||||
inputDir,
|
||||
'--dump-sourcemap',
|
||||
...sourceMapArgs,
|
||||
'--dump-assetmap',
|
||||
`--platform=${platformFlag}`,
|
||||
...(clearCache ? ['--clear'] : []),
|
||||
...(noBytecode ? ['--no-bytecode'] : []),
|
||||
],
|
||||
{
|
||||
extraEnv,
|
||||
@@ -1049,11 +1058,6 @@ export const platformDisplayNames: Record<UpdatePublishPlatform, string> = {
|
||||
ios: 'iOS',
|
||||
};
|
||||
|
||||
export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, AppPlatform> = {
|
||||
android: AppPlatform.Android,
|
||||
ios: AppPlatform.Ios,
|
||||
};
|
||||
|
||||
export async function getRuntimeToUpdateRolloutInfoGroupMappingAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
@@ -1118,3 +1122,34 @@ export async function getUpdateRolloutInfoGroupAsync(
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the command line arguments for source map generation in expo export.
|
||||
*
|
||||
* Uses --source-maps if provided, otherwise falls back to --dump-sourcemap.
|
||||
* SDK 55+ supports --source-maps with a value (e.g., 'inline'), but older SDKs
|
||||
* only support it as a boolean flag. Passing a value to older SDKs causes it
|
||||
* to be parsed as the project root positional argument.
|
||||
*/
|
||||
export function getSourceMapExportCommandArgs({
|
||||
sourceMaps,
|
||||
sdkVersion,
|
||||
}: {
|
||||
sourceMaps: string | undefined;
|
||||
sdkVersion: string | undefined;
|
||||
}): string[] {
|
||||
if (!sourceMaps) {
|
||||
return ['--dump-sourcemap'];
|
||||
}
|
||||
|
||||
if (sourceMaps === 'false') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const supportsSourceMapModes = sdkVersion && semver.satisfies(sdkVersion, '>=55.0.0');
|
||||
if (supportsSourceMapModes && sourceMaps !== 'true') {
|
||||
return ['--source-maps', sourceMaps];
|
||||
}
|
||||
|
||||
return ['--source-maps'];
|
||||
}
|
||||
+1
-2
@@ -5,7 +5,6 @@ import {
|
||||
App,
|
||||
DeploymentResult,
|
||||
Runtime,
|
||||
UpdateBranch,
|
||||
UpdateChannel,
|
||||
UpdateChannelRuntimeInsights,
|
||||
UpdateInsights,
|
||||
@@ -126,7 +125,7 @@ function mockUpdateChannel({
|
||||
};
|
||||
}
|
||||
|
||||
function mockUpdateBranches(branchNames: string[]): UpdateBranch[] {
|
||||
function mockUpdateBranches(branchNames: string[]): any[] {
|
||||
return branchNames.map(branchName => ({
|
||||
id: 'f9dc4dbb-663f-4a19-8cf2-a783b484d2db',
|
||||
latestActivity: '2022-12-07T02:24:29.786Z',
|
||||
|
||||
@@ -2,6 +2,7 @@ import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
UpdatePublishPlatform,
|
||||
formatBranch,
|
||||
formatUpdateGroup,
|
||||
formatUpdateTitle,
|
||||
@@ -19,7 +20,6 @@ import {
|
||||
import { RuntimeQuery } from '../graphql/queries/RuntimeQuery';
|
||||
import { UpdateQuery } from '../graphql/queries/UpdateQuery';
|
||||
import Log from '../log';
|
||||
import { UpdatePublishPlatform } from '../project/publish';
|
||||
import formatFields from '../utils/formatFields';
|
||||
import { printJsonOnlyOutput } from '../utils/json';
|
||||
import {
|
||||
@@ -36,9 +36,11 @@ export async function listAndRenderUpdateGroupsOnAppAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
projectId,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
filter?: ViewUpdateGroupsOnAppQueryVariables['filter'];
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
@@ -47,6 +49,7 @@ export async function listAndRenderUpdateGroupsOnAppAsync(
|
||||
limit: paginatedQueryOptions.limit ?? UPDATE_GROUPS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
appId: projectId,
|
||||
filter,
|
||||
});
|
||||
renderUpdateGroupsOnApp({ updateGroups, paginatedQueryOptions });
|
||||
} else {
|
||||
@@ -54,7 +57,7 @@ export async function listAndRenderUpdateGroupsOnAppAsync(
|
||||
limit: paginatedQueryOptions.limit ?? UPDATE_GROUPS_LIMIT,
|
||||
offset: paginatedQueryOptions.offset,
|
||||
queryToPerform: (limit, offset) =>
|
||||
queryUpdateGroupsOnAppAsync(graphqlClient, { limit, offset, appId: projectId }),
|
||||
queryUpdateGroupsOnAppAsync(graphqlClient, { limit, offset, appId: projectId, filter }),
|
||||
promptOptions: {
|
||||
title: 'Load more update groups?',
|
||||
renderListItems: updateGroups => {
|
||||
@@ -70,10 +73,12 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
|
||||
{
|
||||
projectId,
|
||||
branchName,
|
||||
filter,
|
||||
paginatedQueryOptions,
|
||||
}: {
|
||||
projectId: string;
|
||||
branchName: string;
|
||||
filter?: ViewUpdateGroupsOnBranchQueryVariables['filter'];
|
||||
paginatedQueryOptions: PaginatedQueryOptions;
|
||||
}
|
||||
): Promise<void> {
|
||||
@@ -83,6 +88,7 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
|
||||
offset: paginatedQueryOptions.offset,
|
||||
appId: projectId,
|
||||
branchName,
|
||||
filter,
|
||||
});
|
||||
renderUpdateGroupsOnBranch({ updateGroups, branchName, paginatedQueryOptions });
|
||||
} else {
|
||||
@@ -95,6 +101,7 @@ export async function listAndRenderUpdateGroupsOnBranchAsync(
|
||||
offset,
|
||||
appId: projectId,
|
||||
branchName,
|
||||
filter,
|
||||
}),
|
||||
promptOptions: {
|
||||
title: 'Load more update groups?',
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
selectRuntimeAndGetLatestUpdateGroupForEachPublishPlatformOnBranchAsync,
|
||||
selectUpdateGroupOnBranchAsync,
|
||||
} from './queries';
|
||||
import { truncateString as truncateUpdateMessage } from './utils';
|
||||
import { UpdatePublishPlatform, truncateString as truncateUpdateMessage } from './utils';
|
||||
import { selectBranchOnAppAsync } from '../branch/queries';
|
||||
import { getUpdateGroupUrl } from '../build/utils/url';
|
||||
import { selectChannelOnAppAsync } from '../channel/queries';
|
||||
@@ -20,7 +20,7 @@ import { UpdateQuery } from '../graphql/queries/UpdateQuery';
|
||||
import Log, { link } from '../log';
|
||||
import { ora } from '../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import { UpdatePublishPlatform, getUpdateRolloutInfoGroupAsync } from '../project/publish';
|
||||
import { getUpdateRolloutInfoGroupAsync } from '../project/publish';
|
||||
import { promptAsync } from '../prompts';
|
||||
import {
|
||||
CodeSigningInfo,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import nullthrows from 'nullthrows';
|
||||
|
||||
import { getUpdateJsonInfosForUpdates } from './utils';
|
||||
import { UpdatePublishPlatform, getUpdateJsonInfosForUpdates } from './utils';
|
||||
import { getUpdateGroupUrl } from '../build/utils/url';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import fetch from '../fetch';
|
||||
@@ -12,7 +12,6 @@ import { ora } from '../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
|
||||
import {
|
||||
RuntimeVersionInfo,
|
||||
UpdatePublishPlatform,
|
||||
getRuntimeToPlatformsAndFingerprintInfoMappingFromRuntimeVersionInfoObjects,
|
||||
} from '../project/publish';
|
||||
import {
|
||||
|
||||
@@ -3,14 +3,19 @@ import { format } from '@expo/timeago.js';
|
||||
import chalk from 'chalk';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AppPlatform,
|
||||
Robot,
|
||||
SsoUser,
|
||||
Update,
|
||||
UpdateBranchFragment,
|
||||
UpdateFragment,
|
||||
UpdatePublishMutation,
|
||||
User,
|
||||
} from '../graphql/generated';
|
||||
import { AssetQuery } from '../graphql/queries/AssetQuery';
|
||||
import { BranchQuery } from '../graphql/queries/BranchQuery';
|
||||
import { learnMore } from '../log';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { getActorDisplayName } from '../user/User';
|
||||
@@ -269,3 +274,86 @@ export function getBranchDescription(branch: UpdateBranchFragment): FormattedBra
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function isBundleDiffingEnabled(exp: ExpoConfig): boolean {
|
||||
return (exp.updates as any)?.enableBsdiffPatchSupport === true;
|
||||
}
|
||||
|
||||
// Make authenticated requests to the launch asset URL with diffing headers
|
||||
export async function prewarmDiffingAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string,
|
||||
newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups']
|
||||
): Promise<void> {
|
||||
const DUMMY_EMBEDDED_UPDATE_ID = '00000000-0000-0000-0000-000000000000';
|
||||
|
||||
const toPrewarm = [] as {
|
||||
update: UpdatePublishMutation['updateBranch']['publishUpdateGroups'][0];
|
||||
launchAssetKey: string;
|
||||
}[];
|
||||
|
||||
for (const update of newUpdates) {
|
||||
const manifest = JSON.parse(update.manifestFragment);
|
||||
const launchAssetKey: string | undefined = manifest.launchAsset?.storageKey;
|
||||
const requestedUpdateId: string = update.id;
|
||||
if (!launchAssetKey || !requestedUpdateId) {
|
||||
continue;
|
||||
}
|
||||
toPrewarm.push({
|
||||
update,
|
||||
launchAssetKey,
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.allSettled(
|
||||
toPrewarm.map(async ({ update, launchAssetKey }) => {
|
||||
try {
|
||||
// Check to see if there's a second most recent update so we can pre-emptively generate a patch for it
|
||||
const updatePublishPlatform = update.platform as UpdatePublishPlatform;
|
||||
const updateIds = await BranchQuery.getUpdateIdsOnBranchAsync(graphqlClient, {
|
||||
appId,
|
||||
branchName: update.branch.name,
|
||||
platform: updatePublishPlatformToAppPlatform[updatePublishPlatform],
|
||||
runtimeVersion: update.runtimeVersion,
|
||||
limit: 2,
|
||||
});
|
||||
if (updateIds.length !== 2) {
|
||||
return;
|
||||
}
|
||||
const nextMostRecentUpdateId = updateIds[1];
|
||||
|
||||
const signed = await AssetQuery.getSignedUrlsAsync(graphqlClient, update.id, [
|
||||
launchAssetKey,
|
||||
]);
|
||||
const first = signed?.[0];
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
...(first.headers as Record<string, string> | undefined),
|
||||
'expo-current-update-id': nextMostRecentUpdateId,
|
||||
'expo-requested-update-id': update.id,
|
||||
'expo-embedded-update-id': DUMMY_EMBEDDED_UPDATE_ID,
|
||||
'a-im': 'bsdiff',
|
||||
};
|
||||
|
||||
await fetch(first.url, {
|
||||
method: 'HEAD',
|
||||
headers,
|
||||
signal: AbortSignal.timeout(2500),
|
||||
});
|
||||
} catch {
|
||||
// ignore errors, best-effort optimization
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// update publish does not currently support web
|
||||
export type UpdatePublishPlatform = 'ios' | 'android';
|
||||
|
||||
export const updatePublishPlatformToAppPlatform: Record<UpdatePublishPlatform, AppPlatform> = {
|
||||
android: AppPlatform.Android,
|
||||
ios: AppPlatform.Ios,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import path from 'path';
|
||||
|
||||
import { ImageNonPngError, ImageTransparencyError, ensurePNGIsNotTransparentAsync } from '../image';
|
||||
|
||||
const TEST_SERVER_PORT = 2137;
|
||||
const TEST_SERVER_PORT = 9999;
|
||||
|
||||
const fixturesPath = path.join(__dirname, 'fixtures');
|
||||
const transparentPngPath = path.join(fixturesPath, 'icon-alpha.png');
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
AccountUsageForOverageWarningQuery,
|
||||
EasService,
|
||||
EasServiceMetric,
|
||||
EstimatedUsage,
|
||||
UsageMetricType,
|
||||
} from '../../../graphql/generated';
|
||||
import { AccountUsageQuery } from '../../../graphql/queries/AccountUsageQuery';
|
||||
import Log, { link } from '../../../log';
|
||||
import {
|
||||
createProgressBar,
|
||||
displayOverageWarning,
|
||||
maybeWarnAboutUsageOveragesAsync,
|
||||
} from '../checkForOverages';
|
||||
|
||||
jest.mock('../../../graphql/queries/AccountUsageQuery');
|
||||
jest.mock('../../../log', () => ({
|
||||
...jest.requireActual('../../../log'),
|
||||
warn: jest.fn(),
|
||||
newLine: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
link: jest.fn((url: string, opts?: { text?: string }) => opts?.text ?? url),
|
||||
}));
|
||||
|
||||
function createMockPlanMetric({
|
||||
value = 50,
|
||||
limit = 100,
|
||||
}: Partial<Pick<EstimatedUsage, 'value' | 'limit'>> = {}): EstimatedUsage {
|
||||
return {
|
||||
__typename: 'EstimatedUsage',
|
||||
id: 'plan-metric-id',
|
||||
service: EasService.Builds,
|
||||
serviceMetric: EasServiceMetric.Builds,
|
||||
metricType: UsageMetricType.Build,
|
||||
value,
|
||||
limit,
|
||||
platformBreakdown: null,
|
||||
};
|
||||
}
|
||||
|
||||
function createMockAccountUsage({
|
||||
id = 'account-id',
|
||||
name = 'test-account',
|
||||
subscriptionName = 'Free',
|
||||
buildPlanMetrics = [],
|
||||
}: {
|
||||
id?: string;
|
||||
name?: string;
|
||||
subscriptionName?: string;
|
||||
buildPlanMetrics?: EstimatedUsage[];
|
||||
} = {}): AccountUsageForOverageWarningQuery['account']['byId'] {
|
||||
return {
|
||||
__typename: 'Account',
|
||||
id,
|
||||
name,
|
||||
subscription: {
|
||||
__typename: 'SubscriptionDetails',
|
||||
id: 'sub-id',
|
||||
name: subscriptionName,
|
||||
},
|
||||
usageMetrics: {
|
||||
EAS_BUILD: {
|
||||
__typename: 'UsageMetricTotal',
|
||||
id: 'metric-id',
|
||||
planMetrics: buildPlanMetrics,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe('maybeWarnAboutUsageOveragesAsync', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockGetUsageForOverageWarningAsync = jest.mocked(
|
||||
AccountUsageQuery.getUsageForOverageWarningAsync
|
||||
);
|
||||
const mockWarn = jest.mocked(Log.warn);
|
||||
const mockDebug = jest.mocked(Log.debug);
|
||||
|
||||
beforeEach(() => {
|
||||
mockGetUsageForOverageWarningAsync.mockClear();
|
||||
mockWarn.mockClear();
|
||||
mockDebug.mockClear();
|
||||
});
|
||||
|
||||
it('displays a warning for a Free plan with high build usage', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 85 })],
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockGetUsageForOverageWarningAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'account-id',
|
||||
expect.any(Date)
|
||||
);
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a warning for a Starter plan with high build usage', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
subscriptionName: 'Starter',
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 90 })],
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 90% of your included build credits for this month.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Additional usage beyond your limit will be charged at pay-as-you-go rates.'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
it('does not display a warning when usage is below threshold', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(
|
||||
createMockAccountUsage({
|
||||
buildPlanMetrics: [createMockPlanMetric({ value: 50 })],
|
||||
})
|
||||
);
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('handles errors gracefully', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockRejectedValue(new Error('Network error'));
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
expect(mockDebug).toHaveBeenCalledWith(expect.stringContaining('Failed to fetch usage data'));
|
||||
});
|
||||
|
||||
it('does not display a warning when there are no plan metrics', async () => {
|
||||
mockGetUsageForOverageWarningAsync.mockResolvedValue(createMockAccountUsage());
|
||||
|
||||
await maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
accountId: 'account-id',
|
||||
});
|
||||
|
||||
expect(mockWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProgressBar', () => {
|
||||
it('creates a progress bar with correct fill for a given percentage', () => {
|
||||
const bar = createProgressBar(85, 20);
|
||||
expect(bar).toBe('█████████████████░░░');
|
||||
});
|
||||
|
||||
it('creates a progress bar with correct fill for 100%', () => {
|
||||
const bar = createProgressBar(100, 20);
|
||||
expect(bar).toBe('████████████████████');
|
||||
});
|
||||
|
||||
it('creates a progress bar with correct fill for 0%', () => {
|
||||
const bar = createProgressBar(0, 20);
|
||||
expect(bar).toBe('░░░░░░░░░░░░░░░░░░░░');
|
||||
});
|
||||
|
||||
it('handles custom width', () => {
|
||||
const bar = createProgressBar(50, 10);
|
||||
expect(bar).toBe('█████░░░░░');
|
||||
});
|
||||
});
|
||||
|
||||
describe('displayOverageWarning', () => {
|
||||
const mockWarn = Log.warn as jest.MockedFunction<typeof Log.warn>;
|
||||
const mockLink = link as jest.MockedFunction<typeof link>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWarn.mockClear();
|
||||
mockLink.mockClear();
|
||||
});
|
||||
|
||||
it('displays a warning for a Free plan', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: true,
|
||||
name: 'test-account',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You won't be able to start new builds once you reach the limit.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Upgrade your plan to continue service.')
|
||||
);
|
||||
});
|
||||
|
||||
it('displays a warning for a paid plan', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: false,
|
||||
name: 'test-account',
|
||||
});
|
||||
|
||||
expect(mockWarn).toHaveBeenCalledTimes(2);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("You've used 85% of your included build credits for this month.")
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
'Additional usage beyond your limit will be charged at pay-as-you-go rates.'
|
||||
)
|
||||
);
|
||||
expect(mockWarn).toHaveBeenCalledWith(expect.stringContaining('See usage in billing.'));
|
||||
expect(mockWarn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('██████████████████████████░░░░')
|
||||
);
|
||||
});
|
||||
|
||||
it('includes correct account name in billing URL', () => {
|
||||
displayOverageWarning({
|
||||
percentUsed: 85,
|
||||
hasFreePlan: true,
|
||||
name: 'my-custom-account',
|
||||
});
|
||||
|
||||
expect(mockLink).toHaveBeenCalledWith(
|
||||
'https://expo.dev/accounts/my-custom-account/settings/billing',
|
||||
expect.objectContaining({ text: 'Upgrade your plan to continue service.' })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AccountUsageQuery } from '../../graphql/queries/AccountUsageQuery';
|
||||
import Log, { link } from '../../log';
|
||||
|
||||
const THRESHOLD_PERCENT = 85;
|
||||
|
||||
export async function maybeWarnAboutUsageOveragesAsync({
|
||||
graphqlClient,
|
||||
accountId,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
accountId: string;
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const currentDate = new Date();
|
||||
const {
|
||||
name,
|
||||
subscription,
|
||||
usageMetrics: { EAS_BUILD },
|
||||
} = await AccountUsageQuery.getUsageForOverageWarningAsync(
|
||||
graphqlClient,
|
||||
accountId,
|
||||
currentDate
|
||||
);
|
||||
|
||||
const planMetric = EAS_BUILD?.planMetrics?.[0];
|
||||
if (!planMetric || !subscription) {
|
||||
return;
|
||||
}
|
||||
|
||||
const percentUsed = calculatePercentUsed(planMetric.value, planMetric.limit);
|
||||
if (percentUsed >= THRESHOLD_PERCENT) {
|
||||
const hasFreePlan = subscription.name === 'Free';
|
||||
displayOverageWarning({ percentUsed, hasFreePlan, name });
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail if we can't fetch usage data - we don't want to block the user's workflow
|
||||
Log.debug(`Failed to fetch usage data: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function calculatePercentUsed(value: number, limit: number): number {
|
||||
if (limit === 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(Math.floor((value / limit) * 100), 100);
|
||||
}
|
||||
|
||||
export function createProgressBar(percentUsed: number, width: number = 30): string {
|
||||
const filledWidth = Math.round((percentUsed / 100) * width);
|
||||
const emptyWidth = width - filledWidth;
|
||||
const filled = '█'.repeat(filledWidth);
|
||||
const empty = '░'.repeat(emptyWidth);
|
||||
return `${filled}${empty}`;
|
||||
}
|
||||
|
||||
export function displayOverageWarning({
|
||||
percentUsed,
|
||||
hasFreePlan,
|
||||
name,
|
||||
}: {
|
||||
percentUsed: number;
|
||||
hasFreePlan: boolean;
|
||||
name: string;
|
||||
}): void {
|
||||
Log.warn(
|
||||
chalk.bold(`You've used ${percentUsed}% of your included build credits for this month. `) +
|
||||
createProgressBar(percentUsed)
|
||||
);
|
||||
|
||||
const billingUrl = `https://expo.dev/accounts/${name}/settings/billing`;
|
||||
const warning = hasFreePlan
|
||||
? "You won't be able to start new builds once you reach the limit. " +
|
||||
link(billingUrl, { text: 'Upgrade your plan to continue service.', dim: false })
|
||||
: 'Additional usage beyond your limit will be charged at pay-as-you-go rates. ' +
|
||||
link(billingUrl, {
|
||||
text: 'See usage in billing.',
|
||||
dim: false,
|
||||
});
|
||||
|
||||
Log.warn(warning);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
# CLAUDE.md - eas-json
|
||||
|
||||
This file provides guidance for working with the **@expo/eas-json** package.
|
||||
|
||||
## Overview
|
||||
|
||||
A library for parsing and validating `eas.json` configuration files. Published as `@expo/eas-json` on npm.
|
||||
|
||||
- Validates `eas.json` configuration files using **Joi schemas**
|
||||
- Manages build, submit, and deploy profiles
|
||||
- Supports **JSON5** via golden-fleece
|
||||
- Used by eas-cli and other Expo tools
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
yarn build # TypeScript compilation (src → build)
|
||||
yarn watch # Watch mode
|
||||
yarn typecheck # Type check
|
||||
yarn test # Run tests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Schema Validation
|
||||
|
||||
The package uses **Joi** for schema validation:
|
||||
|
||||
**Location**: `src/schema.ts`
|
||||
|
||||
The schema defines valid structure for:
|
||||
- Build profiles
|
||||
- Submit profiles
|
||||
- Deploy profiles
|
||||
- CLI version constraints
|
||||
|
||||
### JSON5 Support
|
||||
|
||||
Uses **golden-fleece** library to support JSON5 syntax in `eas.json`:
|
||||
- Comments
|
||||
- Trailing commas
|
||||
- Unquoted keys
|
||||
- Single quotes
|
||||
|
||||
## Common Usage Patterns
|
||||
|
||||
### Reading eas.json
|
||||
|
||||
```typescript
|
||||
import { EasJsonAccessor, EasJsonUtils } from '@expo/eas-json';
|
||||
|
||||
// Read eas.json from project directory
|
||||
const easJson = await EasJsonUtils.readAsync(projectDir);
|
||||
|
||||
// Access build profiles
|
||||
const buildProfile = easJson.build?.[profileName];
|
||||
if (!buildProfile) {
|
||||
throw new Error(`Build profile "${profileName}" not found`);
|
||||
}
|
||||
|
||||
// Access submit profiles
|
||||
const submitProfile = easJson.submit?.[profileName];
|
||||
|
||||
// Check CLI version constraint
|
||||
const cliVersion = easJson.cli?.version;
|
||||
```
|
||||
|
||||
### Using EasJsonAccessor
|
||||
|
||||
```typescript
|
||||
import { EasJsonAccessor } from '@expo/eas-json';
|
||||
|
||||
// Create accessor from project path
|
||||
const accessor = EasJsonAccessor.fromProjectPath(projectDir);
|
||||
|
||||
// Read raw eas.json
|
||||
const rawJson = await accessor.readRawAsync();
|
||||
|
||||
// Read and validate eas.json
|
||||
const easJson = await accessor.readAsync();
|
||||
|
||||
// Write eas.json
|
||||
await accessor.writeAsync(easJson);
|
||||
```
|
||||
|
||||
### Validating eas.json
|
||||
|
||||
```typescript
|
||||
import { validateEasJsonAsync } from '@expo/eas-json';
|
||||
|
||||
try {
|
||||
await validateEasJsonAsync(projectDir);
|
||||
console.log('eas.json is valid');
|
||||
} catch (error) {
|
||||
console.error('Validation error:', error.message);
|
||||
}
|
||||
```
|
||||
|
||||
## Schema Structure
|
||||
|
||||
### Build Profiles
|
||||
|
||||
Build profiles can contain:
|
||||
- `distribution`: Distribution type (store, internal, simulator)
|
||||
- `platform`: Platform-specific configuration (iOS/Android)
|
||||
- `env`: Environment variables
|
||||
- `node`: Node.js version
|
||||
- `yarn`: Yarn version
|
||||
- `npm`: npm version
|
||||
- `cache`: Cache configuration
|
||||
- And many more build-specific options
|
||||
|
||||
### Submit Profiles
|
||||
|
||||
Submit profiles contain app store submission configuration:
|
||||
- iOS: App Store Connect credentials, ASC App ID
|
||||
- Android: Google Service Account key, track
|
||||
|
||||
### Deploy Profiles
|
||||
|
||||
Deploy profiles for EAS Deploy (worker deployments):
|
||||
- `region`: Deployment region
|
||||
- `nodeVersion`: Node.js version
|
||||
- Environment configuration
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **JSON5**: Full JSON5 syntax support via golden-fleece
|
||||
- **Validation**: Joi schema validation with helpful error messages
|
||||
- **Types**: Comprehensive TypeScript types exported
|
||||
- **CLI Version**: Supports enforcing eas-cli version via `cli.version` field
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "16.23.0",
|
||||
"version": "16.30.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
@@ -23,7 +23,7 @@
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"memfs": "3.4.13",
|
||||
"rimraf": "3.0.2",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -32,7 +32,11 @@
|
||||
"license": "MIT",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts",
|
||||
"repository": "expo/eas-cli",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/expo/eas-cli.git",
|
||||
"directory": "packages/eas-json"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
||||
@@ -49,9 +53,5 @@
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.11.0",
|
||||
"yarn": "1.22.21"
|
||||
}
|
||||
}
|
||||
@@ -522,7 +522,8 @@
|
||||
"**Closed testing** releases are available to a limited number of testers that you choose, who can test a pre-release version of your app and submit feedback.",
|
||||
"**Internal testing** releases are available to up to 100 testers that you choose.",
|
||||
"**Production releases** are available to all Google Play users in your chosen countries."
|
||||
]
|
||||
],
|
||||
"default": "internal"
|
||||
},
|
||||
"releaseStatus": {
|
||||
"enum": ["draft", "inProgress", "halted", "completed"],
|
||||
@@ -533,7 +534,8 @@
|
||||
"The release's APKs are being served to a fraction of users, if set.",
|
||||
"The release's APKs will no longer be served to users. Users who already have these APKs are unaffected.",
|
||||
"The release will have no further changes. Its APKs are being served to all users, unless they are eligible to APKs of a more recent release."
|
||||
]
|
||||
],
|
||||
"default": "completed"
|
||||
},
|
||||
"rollout": {
|
||||
"type": "number",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/.bundle/
|
||||
/.yardoc
|
||||
/_yardoc/
|
||||
/coverage/
|
||||
/doc/
|
||||
/pkg/
|
||||
/spec/reports/
|
||||
/tmp/
|
||||
|
||||
# rspec failure tracking
|
||||
.rspec_status
|
||||
@@ -0,0 +1,3 @@
|
||||
--format documentation
|
||||
--color
|
||||
--require spec_helper
|
||||
@@ -0,0 +1,8 @@
|
||||
Metrics/BlockLength:
|
||||
Exclude:
|
||||
- "spec/**/*"
|
||||
Lint/AmbiguousBlockAssociation:
|
||||
Exclude:
|
||||
- "spec/**/*"
|
||||
Metrics/MethodLength:
|
||||
Max: 20
|
||||
@@ -0,0 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
||||
|
||||
# Specify your gem's dependencies in expo-cocoapods-proxy.gemspec
|
||||
gemspec
|
||||
|
||||
gem 'rubocop', '~> 1.48', group: :development
|
||||
|
||||
gem 'cocoapods', '~> 1.12'
|
||||
@@ -0,0 +1,142 @@
|
||||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
expo-cocoapods-proxy (0.1.0)
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.6)
|
||||
rexml
|
||||
activesupport (7.0.8)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 1.6, < 2)
|
||||
minitest (>= 5.1)
|
||||
tzinfo (~> 2.0)
|
||||
addressable (2.8.1)
|
||||
public_suffix (>= 2.0.2, < 6.0)
|
||||
algoliasearch (1.27.5)
|
||||
httpclient (~> 2.8, >= 2.8.3)
|
||||
json (>= 1.5.1)
|
||||
ast (2.4.2)
|
||||
atomos (0.1.3)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.12.0)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.12.0)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 1.6.0, < 2.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
cocoapods-search (>= 1.0.0, < 2.0)
|
||||
cocoapods-trunk (>= 1.6.0, < 2.0)
|
||||
cocoapods-try (>= 1.1.0, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
escape (~> 0.0.4)
|
||||
fourflusher (>= 2.3.0, < 3.0)
|
||||
gh_inspector (~> 1.0)
|
||||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.21.0, < 2.0)
|
||||
cocoapods-core (1.12.0)
|
||||
activesupport (>= 5.0, < 8)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
concurrent-ruby (~> 1.1)
|
||||
fuzzy_match (~> 2.0.4)
|
||||
nap (~> 1.0)
|
||||
netrc (~> 0.11)
|
||||
public_suffix (~> 4.0)
|
||||
typhoeus (~> 1.0)
|
||||
cocoapods-deintegrate (1.0.5)
|
||||
cocoapods-downloader (1.6.3)
|
||||
cocoapods-plugins (1.0.0)
|
||||
nap
|
||||
cocoapods-search (1.0.1)
|
||||
cocoapods-trunk (1.6.0)
|
||||
nap (>= 0.8, < 2.0)
|
||||
netrc (~> 0.11)
|
||||
cocoapods-try (1.2.0)
|
||||
colored2 (3.1.2)
|
||||
concurrent-ruby (1.2.2)
|
||||
diff-lcs (1.5.0)
|
||||
escape (0.0.4)
|
||||
ethon (0.16.0)
|
||||
ffi (>= 1.15.0)
|
||||
ffi (1.15.5)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
json (2.6.3)
|
||||
minitest (5.20.0)
|
||||
molinillo (0.8.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
netrc (0.11.0)
|
||||
parallel (1.22.1)
|
||||
parser (3.2.1.1)
|
||||
ast (~> 2.4.1)
|
||||
public_suffix (4.0.7)
|
||||
rainbow (3.1.1)
|
||||
rake (10.5.0)
|
||||
regexp_parser (2.7.0)
|
||||
rexml (3.3.6)
|
||||
strscan
|
||||
rspec (3.12.0)
|
||||
rspec-core (~> 3.12.0)
|
||||
rspec-expectations (~> 3.12.0)
|
||||
rspec-mocks (~> 3.12.0)
|
||||
rspec-core (3.12.1)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-expectations (3.12.2)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-mocks (3.12.4)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.48.1)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.2.0.0)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.8, < 3.0)
|
||||
rexml (>= 3.2.5, < 4.0)
|
||||
rubocop-ast (>= 1.26.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 3.0)
|
||||
rubocop-ast (1.28.0)
|
||||
parser (>= 3.2.1.0)
|
||||
ruby-macho (2.5.1)
|
||||
ruby-progressbar (1.13.0)
|
||||
strscan (3.1.0)
|
||||
typhoeus (1.4.0)
|
||||
ethon (>= 0.9.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.4.2)
|
||||
xcodeproj (1.25.0)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.2, < 4.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
bundler (~> 1.17)
|
||||
cocoapods (~> 1.12)
|
||||
expo-cocoapods-proxy!
|
||||
rake (~> 10.0)
|
||||
rspec (~> 3.0)
|
||||
rubocop (~> 1.48)
|
||||
|
||||
BUNDLED WITH
|
||||
2.1.4
|
||||
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/gem_tasks'
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
RSpec::Core::RakeTask.new(:spec)
|
||||
|
||||
task default: :spec
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'expo/cocoapods/proxy'
|
||||
|
||||
# You can add fixtures and/or initialization code here to make experimenting
|
||||
# with your gem easier. You can also use a different console, if you like.
|
||||
|
||||
# (If you use this, don't forget to add pry to your Gemfile!)
|
||||
# require "pry"
|
||||
# Pry.start
|
||||
|
||||
require 'irb'
|
||||
IRB.start(__FILE__)
|
||||
Loaded 100 of 176 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user