mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-15 03:38:08 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
184616e874 | ||
|
|
6bb09c4e99 | ||
|
|
81add86c61 | ||
|
|
4bb1c784d8 | ||
|
|
322082c28b | ||
|
|
5db1d21aa5 | ||
|
|
0db5585b20 | ||
|
|
10ec54345a | ||
|
|
27b4710264 | ||
|
|
4132e92da5 | ||
|
|
264340a717 | ||
|
|
1d09c2c97d |
No files matched your search
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
node: ['18', '20', '22']
|
||||
node: ['20', '22']
|
||||
include:
|
||||
- node: '20'
|
||||
coverage: true
|
||||
|
||||
+22
-1
@@ -8,12 +8,33 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
|
||||
- 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
|
||||
|
||||
- Multiple bugfixes and small features for the `eas:new` command: handle project name and directory generation more smoothly; support `bun` as a package manager; add `--package-manager` flag with a default to `npm`; ensure workflow files are in the correct location after build; update the generated project's README with the provided package manager; ensure `app.json`'s `scheme` is valid; silence lots of noisy output. ([#3205](https://github.com/expo/eas-cli/pull/3205) by [@mackenco](https://github.com/mackenco))
|
||||
|
||||
## [16.23.0](https://github.com/expo/eas-cli/releases/tag/v16.23.0) - 2025-10-14
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Support custom environment variable environments ([#3220](https://github.com/expo/eas-cli/pull/3220) by [@kadikraman](https://github.com/kadikraman))
|
||||
- Add --force flag to eas env:push command to bypass confirmation prompts. ([#3135](https://github.com/expo/eas-cli/pull/3135) by [@giorgiofellipe](https://github.com/giorgiofellipe))
|
||||
|
||||
## [16.22.0](https://github.com/expo/eas-cli/releases/tag/v16.22.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
- Improvements to workflow:create. ([#3194](https://github.com/expo/eas-cli/pull/3194) by [@douglowder](https://github.com/douglowder))
|
||||
|
||||
## [16.21.0](https://github.com/expo/eas-cli/releases/tag/v16.21.0) - 2025-10-13
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "16.22.0",
|
||||
"version": "16.24.0",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+208
-207
File diff suppressed because it is too large.
Load diff
Generated
+566
-10
@@ -2275,6 +2275,30 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "viewerNotificationPreferences",
|
||||
"description": "Notification preferences of the viewer for this account",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "NotificationPreferenceItem",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "viewerUserPermission",
|
||||
"description": "Permission info for the viewer on this account",
|
||||
@@ -3728,6 +3752,162 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AccountNotificationPreference",
|
||||
"description": "Account-level notification preference",
|
||||
"fields": [
|
||||
{
|
||||
"name": "account",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Account",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationEvent",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
{
|
||||
"kind": "INTERFACE",
|
||||
"name": "NotificationPreference",
|
||||
"ofType": null
|
||||
}
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AccountNotificationPreferenceInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "accountId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationEvent",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AccountNotificationSubscriptionInput",
|
||||
@@ -10753,6 +10933,30 @@
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "No longer supported"
|
||||
},
|
||||
{
|
||||
"name": "viewerNotificationPreferences",
|
||||
"description": "Notification preferences of the viewer for this app",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "NotificationPreferenceItem",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "webhooks",
|
||||
"description": "Webhooks for an app",
|
||||
@@ -12511,6 +12715,162 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppNotificationPreference",
|
||||
"description": "App-level notification preference",
|
||||
"fields": [
|
||||
{
|
||||
"name": "app",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "App",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationEvent",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
{
|
||||
"kind": "INTERFACE",
|
||||
"name": "NotificationPreference",
|
||||
"ofType": null
|
||||
}
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppNotificationPreferenceInput",
|
||||
"description": null,
|
||||
"fields": null,
|
||||
"inputFields": [
|
||||
{
|
||||
"name": "appId",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationEvent",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppNotificationSubscriptionInput",
|
||||
@@ -39524,8 +39884,8 @@
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "This method is no longer supported."
|
||||
},
|
||||
{
|
||||
"name": "setPreferences",
|
||||
@@ -40020,6 +40380,174 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "INTERFACE",
|
||||
"name": "NotificationPreference",
|
||||
"description": "Base interface for notification preferences",
|
||||
"fields": [
|
||||
{
|
||||
"name": "enabled",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "event",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationEvent",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "type",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "NotificationType",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": [
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AccountNotificationPreference",
|
||||
"ofType": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppNotificationPreference",
|
||||
"ofType": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "UNION",
|
||||
"name": "NotificationPreferenceItem",
|
||||
"description": "Union type representing either account-level or app-level notification preferences",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": [
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AccountNotificationPreference",
|
||||
"ofType": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AppNotificationPreference",
|
||||
"ofType": null
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "NotificationPreferenceMutation",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "setAccountNotificationPreference",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AccountNotificationPreferenceInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "NotificationPreferenceItem",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "setAppNotificationPreference",
|
||||
"description": null,
|
||||
"args": [
|
||||
{
|
||||
"name": "input",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "INPUT_OBJECT",
|
||||
"name": "AppNotificationPreferenceInput",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "UNION",
|
||||
"name": "NotificationPreferenceItem",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "NotificationSubscription",
|
||||
@@ -43727,6 +44255,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "notificationPreference",
|
||||
"description": "Notification preference management",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "NotificationPreferenceMutation",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "notificationSubscription",
|
||||
"description": "Mutations that modify a NotificationSubscription",
|
||||
@@ -43740,8 +44284,8 @@
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use notificationPreference mutation instead"
|
||||
},
|
||||
{
|
||||
"name": "robot",
|
||||
@@ -46231,8 +46775,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use Account/App.viewerNotificationPreferences instead"
|
||||
},
|
||||
{
|
||||
"name": "pinnedApps",
|
||||
@@ -48462,6 +49006,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "jobRun",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "JobRun",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "logFiles",
|
||||
"description": null,
|
||||
@@ -53666,8 +54222,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use Account/App.viewerNotificationPreferences instead"
|
||||
},
|
||||
{
|
||||
"name": "pendingUserInvitations",
|
||||
@@ -54494,8 +55050,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
"isDeprecated": true,
|
||||
"deprecationReason": "Use Account/App.viewerNotificationPreferences instead"
|
||||
},
|
||||
{
|
||||
"name": "pinnedApps",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"description": "EAS command line tool",
|
||||
"version": "16.22.0",
|
||||
"version": "16.24.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bin": {
|
||||
"eas": "./bin/run"
|
||||
@@ -12,8 +12,8 @@
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "10.0.6",
|
||||
"@expo/config-plugins": "9.0.12",
|
||||
"@expo/eas-build-job": "1.0.231",
|
||||
"@expo/eas-json": "16.20.1",
|
||||
"@expo/eas-build-job": "1.0.239",
|
||||
"@expo/eas-json": "16.23.0",
|
||||
"@expo/env": "^1.0.0",
|
||||
"@expo/json-file": "8.3.3",
|
||||
"@expo/logger": "1.0.221",
|
||||
@@ -71,7 +71,7 @@
|
||||
"node-forge": "1.3.1",
|
||||
"node-stream-zip": "1.15.0",
|
||||
"nullthrows": "1.1.1",
|
||||
"ora": "5.1.0",
|
||||
"ora": "9.0.0",
|
||||
"pkg-dir": "4.2.0",
|
||||
"pngjs": "7.0.0",
|
||||
"promise-limit": "2.7.0",
|
||||
@@ -226,7 +226,7 @@
|
||||
"prepack": "yarn rebuild && node ./scripts/prepack.js",
|
||||
"rebuild": "rimraf build && yarn build",
|
||||
"pretarball-ci": "./scripts/pretarball-ci.sh",
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"build": "tsc --project tsconfig.build.json && yarn copy-new-templates",
|
||||
"build-allow-unused": "tsc --project tsconfig.allowUnused.json",
|
||||
"watch": "yarn build --watch --preserveWatchOutput",
|
||||
"watch-allow-unused": "yarn build-allow-unused --watch --preserveWatchOutput",
|
||||
@@ -235,7 +235,8 @@
|
||||
"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"
|
||||
"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",
|
||||
|
||||
@@ -192,9 +192,16 @@ export function getMockWorkflowRunsFragment(
|
||||
failures?: number;
|
||||
pending?: number;
|
||||
withJobs?: number;
|
||||
withBuildJobs?: number;
|
||||
}
|
||||
): WorkflowRunFragment[] {
|
||||
const { successes = 0, failures = 0, pending = 0, withJobs = 0 } = params ?? {};
|
||||
const {
|
||||
successes = 0,
|
||||
failures = 0,
|
||||
pending = 0,
|
||||
withJobs = 0,
|
||||
withBuildJobs = 0,
|
||||
} = params ?? {};
|
||||
const runs: any[] = [];
|
||||
for (let i = 0; i < successes; i++) {
|
||||
runs.push({
|
||||
@@ -247,10 +254,13 @@ export function getMockWorkflowRunsFragment(
|
||||
for (let i = 0; i < withJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithJobsFragment());
|
||||
}
|
||||
for (let i = 0; i < withBuildJobs; i++) {
|
||||
runs.push(getMockWorkflowRunWithBuildJobsFragment());
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
export function getMockWorkflowJobFragment(
|
||||
export function getMockWorkflowCustomJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
@@ -258,7 +268,7 @@ export function getMockWorkflowJobFragment(
|
||||
key: 'job1',
|
||||
name: 'job1',
|
||||
status: WorkflowJobStatus.Success,
|
||||
type: WorkflowJobType.Build,
|
||||
type: WorkflowJobType.Custom,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
outputs: {},
|
||||
@@ -275,6 +285,18 @@ export function getMockWorkflowJobFragment(
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowBuildJobFragment(
|
||||
runId?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']['jobs'][number] {
|
||||
return {
|
||||
...getMockWorkflowCustomJobFragment(runId),
|
||||
type: WorkflowJobType.Build,
|
||||
outputs: {
|
||||
build_id: 'build1',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunWithJobsFragment(
|
||||
runID?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
|
||||
@@ -303,6 +325,38 @@ export function getMockWorkflowRunWithJobsFragment(
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowJobFragment(id)],
|
||||
jobs: [getMockWorkflowCustomJobFragment(id)],
|
||||
};
|
||||
}
|
||||
|
||||
export function getMockWorkflowRunWithBuildJobsFragment(
|
||||
runID?: string
|
||||
): WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'] {
|
||||
const id = runID ?? 'build1';
|
||||
return {
|
||||
id,
|
||||
status: WorkflowRunStatus.Failure,
|
||||
triggerEventType: WorkflowRunTriggerEventType.Manual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
gitCommitHash: '1234567890',
|
||||
gitCommitMessage: 'commit message',
|
||||
errors: [],
|
||||
workflow: {
|
||||
id: 'build',
|
||||
name: 'build',
|
||||
fileName: 'build.yml',
|
||||
app: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
},
|
||||
jobs: [getMockWorkflowBuildJobFragment(id)],
|
||||
};
|
||||
}
|
||||
@@ -1,15 +1,20 @@
|
||||
import {
|
||||
getMockEmptyWorkflowRunsFragment,
|
||||
getMockWorkflowJobFragment,
|
||||
getMockWorkflowCustomJobFragment,
|
||||
getMockWorkflowRunWithJobsFragment,
|
||||
getMockWorkflowRunsFragment,
|
||||
mockCommandContext,
|
||||
mockProjectId,
|
||||
mockTestCommand,
|
||||
} from './utils';
|
||||
import { fetchRawLogsForJobAsync } from '../../commandUtils/workflow/fetchLogs';
|
||||
import {
|
||||
fetchRawLogsForBuildJobAsync,
|
||||
fetchRawLogsForCustomJobAsync,
|
||||
} from '../../commandUtils/workflow/fetchLogs';
|
||||
import WorkflowLogView from '../../commands/workflow/logs';
|
||||
import { AppPlatform, BuildPriority, BuildStatus } from '../../graphql/generated';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { WorkflowJobQuery } from '../../graphql/queries/WorkflowJobQuery';
|
||||
import { WorkflowRunQuery } from '../../graphql/queries/WorkflowRunQuery';
|
||||
import Log from '../../log';
|
||||
@@ -20,6 +25,7 @@ jest.mock('../../build/ios/version');
|
||||
jest.mock('../../project/applicationIdentifier');
|
||||
jest.mock('../../graphql/queries/AppVersionQuery');
|
||||
jest.mock('../../graphql/queries/AppQuery');
|
||||
jest.mock('../../graphql/queries/BuildQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowJobQuery');
|
||||
jest.mock('../../graphql/queries/WorkflowRunQuery');
|
||||
jest.mock('../../graphql/mutations/AppVersionMutation');
|
||||
@@ -35,7 +41,7 @@ jest.mock('../../commandUtils/workflow/fetchLogs');
|
||||
|
||||
describe(WorkflowLogView, () => {
|
||||
beforeEach(() => {
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowJobFragment());
|
||||
jest.mocked(WorkflowJobQuery.byIdAsync).mockResolvedValue(getMockWorkflowCustomJobFragment());
|
||||
jest
|
||||
.mocked(WorkflowRunQuery.withJobsByIdAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunWithJobsFragment());
|
||||
@@ -98,7 +104,7 @@ describe(WorkflowLogView, () => {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest.mocked(fetchRawLogsForJobAsync).mockResolvedValue(null);
|
||||
jest.mocked(fetchRawLogsForCustomJobAsync).mockResolvedValue(null);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
@@ -130,7 +136,64 @@ describe(WorkflowLogView, () => {
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForJobAsync)
|
||||
.mocked(fetchRawLogsForCustomJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
await cmd.run();
|
||||
expect(AppQuery.byIdWorkflowRunsFilteredByStatusAsync).toHaveBeenCalledWith(
|
||||
ctx.loggedIn.graphqlClient,
|
||||
mockProjectId,
|
||||
undefined,
|
||||
20
|
||||
);
|
||||
expect(promptAsync).toHaveBeenCalledTimes(3);
|
||||
expect(Log.error).not.toHaveBeenCalled();
|
||||
expect(Log.log).toHaveBeenCalledWith(' 2022-01-01T00:00:00.000Z test');
|
||||
});
|
||||
test('view build logs, passing in no parameters, runs found, jobs found, step selected, logs found', async () => {
|
||||
const ctx = mockCommandContext(WorkflowLogView, {
|
||||
projectId: mockProjectId,
|
||||
});
|
||||
const cmd = mockTestCommand(WorkflowLogView, [], ctx);
|
||||
jest
|
||||
.mocked(AppQuery.byIdWorkflowRunsFilteredByStatusAsync)
|
||||
.mockResolvedValue(getMockWorkflowRunsFragment({ withBuildJobs: 1 }));
|
||||
jest.mocked(BuildQuery.byIdAsync).mockResolvedValue({
|
||||
id: 'build1',
|
||||
status: BuildStatus.Finished,
|
||||
priority: BuildPriority.Normal,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
updatedAt: '2022-01-01T00:00:00.000Z',
|
||||
isForIosSimulator: false,
|
||||
project: {
|
||||
id: mockProjectId,
|
||||
__typename: 'App',
|
||||
name: 'App',
|
||||
slug: 'app',
|
||||
ownerAccount: {
|
||||
id: 'account-id',
|
||||
name: 'account-name',
|
||||
__typename: 'Account',
|
||||
},
|
||||
},
|
||||
platform: AppPlatform.Android,
|
||||
logFiles: ['https://example.com/log1'],
|
||||
});
|
||||
let promptCalls = 0;
|
||||
jest.mocked(promptAsync).mockImplementation(async () => {
|
||||
if (promptCalls === 0) {
|
||||
promptCalls++;
|
||||
return { selectedRun: 'build1' };
|
||||
} else if (promptCalls === 1) {
|
||||
promptCalls++;
|
||||
return { selectedJob: 0 };
|
||||
} else {
|
||||
return { selectedStep: 'step1' };
|
||||
}
|
||||
});
|
||||
jest
|
||||
.mocked(fetchRawLogsForBuildJobAsync)
|
||||
.mockResolvedValue(
|
||||
'{"result":"test","marker":"test","buildStepDisplayName":"step1","buildStepInternalId":"step1","time":"2022-01-01T00:00:00.000Z","msg":"test"}'
|
||||
);
|
||||
|
||||
@@ -72,6 +72,7 @@ function createMockBuildFragment({
|
||||
updatedAt: new Date().toISOString(),
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
logFiles: [],
|
||||
priority: BuildPriority.Normal,
|
||||
project: {
|
||||
__typename: 'App',
|
||||
|
||||
@@ -70,6 +70,7 @@ function createMockBuildFragment({
|
||||
return {
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
logFiles: [],
|
||||
platform: AppPlatform.Android,
|
||||
id: buildId ?? uuid(),
|
||||
priority: BuildPriority.Normal,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
return {
|
||||
@@ -30,7 +29,7 @@ export function transformJob(job: Android.Job): AndroidJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Env } from '@expo/eas-build-job';
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
import { EnvironmentVariableEnvironment, isEnvironment } from './utils/environment';
|
||||
import { DefaultEnvironment } from './utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log, { learnMore } from '../log';
|
||||
@@ -48,20 +48,9 @@ async function resolveEnvVarsAsync({
|
||||
projectId: string;
|
||||
}): Promise<Env> {
|
||||
const environment =
|
||||
buildProfile.environment?.toUpperCase() ??
|
||||
buildProfile.environment ??
|
||||
resolveSuggestedEnvironmentForBuildProfileConfiguration(buildProfile);
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
Log.log(
|
||||
`Loaded "env" configuration for the "${buildProfileName}" profile: ${
|
||||
buildProfile.env && Object.keys(buildProfile.env).length > 0
|
||||
? Object.keys(buildProfile.env).join(', ')
|
||||
: 'no environment variables specified'
|
||||
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
|
||||
);
|
||||
return { ...buildProfile.env };
|
||||
}
|
||||
|
||||
try {
|
||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
|
||||
graphqlClient,
|
||||
@@ -128,16 +117,16 @@ async function resolveEnvVarsAsync({
|
||||
|
||||
function resolveSuggestedEnvironmentForBuildProfileConfiguration(
|
||||
buildProfile: BuildProfile
|
||||
): EnvironmentVariableEnvironment {
|
||||
): DefaultEnvironment {
|
||||
const environment =
|
||||
buildProfile.distribution === 'store'
|
||||
? EnvironmentVariableEnvironment.Production
|
||||
? DefaultEnvironment.Production
|
||||
: buildProfile.developmentClient
|
||||
? EnvironmentVariableEnvironment.Development
|
||||
: EnvironmentVariableEnvironment.Preview;
|
||||
? DefaultEnvironment.Development
|
||||
: DefaultEnvironment.Preview;
|
||||
|
||||
Log.log(
|
||||
`Resolved "${environment.toLowerCase()}" environment for the build. ${learnMore(
|
||||
`Resolved "${environment}" environment for the build. ${learnMore(
|
||||
'https://docs.expo.dev/eas/environment-variables/#setting-the-environment-for-your-builds'
|
||||
)}`
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
transformProjectArchive,
|
||||
transformWorkflow,
|
||||
} from '../graphql';
|
||||
import { buildProfileEnvironmentToEnvironment } from '../utils/environment';
|
||||
|
||||
export function transformJob(job: Ios.Job): IosJobInput {
|
||||
return {
|
||||
@@ -32,7 +31,7 @@ export function transformJob(job: Ios.Job): IosJobInput {
|
||||
experimental: job.experimental,
|
||||
mode: transformBuildMode(job.mode),
|
||||
customBuildConfig: job.customBuildConfig,
|
||||
environment: buildProfileEnvironmentToEnvironment(job.environment),
|
||||
environment: job.environment,
|
||||
loggerLevel: job.loggerLevel
|
||||
? loggerLevelToGraphQLWorkerLoggerLevel[job.loggerLevel]
|
||||
: undefined,
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
import { BuildProfile } from '@expo/eas-json';
|
||||
|
||||
type Environment = NonNullable<BuildProfile['environment']>;
|
||||
|
||||
// Default environments
|
||||
export enum EnvironmentVariableEnvironment {
|
||||
export enum DefaultEnvironment {
|
||||
Development = 'development',
|
||||
Preview = 'preview',
|
||||
Production = 'production',
|
||||
}
|
||||
|
||||
const BuildProfileEnvironmentToEnvironment: Record<Environment, EnvironmentVariableEnvironment> = {
|
||||
production: EnvironmentVariableEnvironment.Production,
|
||||
preview: EnvironmentVariableEnvironment.Preview,
|
||||
development: EnvironmentVariableEnvironment.Development,
|
||||
};
|
||||
|
||||
export function isEnvironment(env: string): env is EnvironmentVariableEnvironment {
|
||||
return Object.values(EnvironmentVariableEnvironment).includes(
|
||||
env.toLowerCase() as EnvironmentVariableEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
export function buildProfileEnvironmentToEnvironment(
|
||||
environment: BuildProfile['environment']
|
||||
): EnvironmentVariableEnvironment | null {
|
||||
if (!environment) {
|
||||
return null;
|
||||
}
|
||||
return BuildProfileEnvironmentToEnvironment[environment];
|
||||
}
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
CommandEvent,
|
||||
createAnalyticsAsync,
|
||||
} from '../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
|
||||
import Log, { link } from '../log';
|
||||
import SessionManager from '../user/SessionManager';
|
||||
import { Client } from '../vcs/vcs';
|
||||
@@ -60,7 +59,7 @@ interface BaseGetContextAsyncArgs {
|
||||
|
||||
interface GetContextAsyncArgsWithRequiredServerSideEnvironmentArgument
|
||||
extends BaseGetContextAsyncArgs {
|
||||
withServerSideEnvironment: EnvironmentVariableEnvironment | null;
|
||||
withServerSideEnvironment: string | null;
|
||||
}
|
||||
|
||||
interface GetContextAsyncArgsWithoutServerSideEnvironmentArgument extends BaseGetContextAsyncArgs {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Analytics } from '../../analytics/AnalyticsManager';
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import SessionManager from '../../user/SessionManager';
|
||||
import { Client } from '../../vcs/vcs';
|
||||
|
||||
@@ -11,7 +10,7 @@ export interface ContextOptions {
|
||||
/**
|
||||
* If specified, env variables from the selected environment will be fetched from the server and used to evaluate the dynamic config.
|
||||
*/
|
||||
withServerSideEnvironment?: EnvironmentVariableEnvironment | null;
|
||||
withServerSideEnvironment?: string | null;
|
||||
}
|
||||
|
||||
export default abstract class ContextField<T> {
|
||||
|
||||
+4
-12
@@ -1,23 +1,15 @@
|
||||
import { ExpoGraphqlClient } from './createGraphqlClient';
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
|
||||
const cachedServerSideEnvironmentVariables: Record<
|
||||
EnvironmentVariableEnvironment,
|
||||
Record<string, string> | null
|
||||
> = {
|
||||
[EnvironmentVariableEnvironment.Development]: null,
|
||||
[EnvironmentVariableEnvironment.Preview]: null,
|
||||
[EnvironmentVariableEnvironment.Production]: null,
|
||||
};
|
||||
const cachedServerSideEnvironmentVariables: Record<string, Record<string, string> | undefined> = {};
|
||||
|
||||
export async function loadServerSideEnvironmentVariablesAsync({
|
||||
environment,
|
||||
projectId,
|
||||
graphqlClient,
|
||||
}: {
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
projectId: string;
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
}): Promise<Record<string, string>> {
|
||||
@@ -42,13 +34,13 @@ export async function loadServerSideEnvironmentVariablesAsync({
|
||||
|
||||
if (Object.keys(serverEnvVars).length > 0) {
|
||||
Log.log(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment.toLowerCase()}" environment on EAS: ${Object.keys(
|
||||
`Environment variables with visibility "Plain text" and "Sensitive" loaded from the "${environment}" environment on EAS: ${Object.keys(
|
||||
serverEnvVars
|
||||
).join(', ')}.`
|
||||
);
|
||||
} else {
|
||||
Log.log(
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment.toLowerCase()}" environment on EAS.`
|
||||
`No environment variables with visibility "Plain text" and "Sensitive" found for the "${environment}" environment on EAS.`
|
||||
);
|
||||
}
|
||||
Log.newLine();
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
|
||||
|
||||
// NOTE: not exactly true, but, provided mapToLowercase and upperCaseAsync
|
||||
// are used in tandem, it saves on unnecessary typying in commands
|
||||
async function upperCaseAsync<T>(input: string): Promise<T> {
|
||||
return input.toUpperCase() as T;
|
||||
}
|
||||
|
||||
function mapToLowercase<T extends string>(options: T[]): T[] {
|
||||
return options.map(option => option.toLowerCase()) as T[];
|
||||
}
|
||||
|
||||
export const EasNonInteractiveAndJsonFlags = {
|
||||
json: Flags.boolean({
|
||||
description: 'Enable JSON output, non-JSON messages will be printed to stderr.',
|
||||
@@ -23,20 +11,17 @@ export const EasNonInteractiveAndJsonFlags = {
|
||||
};
|
||||
|
||||
export const EasEnvironmentFlagParameters = {
|
||||
description: "Environment variable's environment",
|
||||
options: mapToLowercase([
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
EnvironmentVariableEnvironment.Preview,
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
]),
|
||||
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
|
||||
};
|
||||
|
||||
export const EASEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>(EasEnvironmentFlagParameters),
|
||||
environment: Flags.string({
|
||||
description: "Environment variable's environment, e.g. 'production', 'preview', 'development'",
|
||||
}),
|
||||
};
|
||||
|
||||
export const EASMultiEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
multiple: true,
|
||||
}),
|
||||
@@ -80,12 +65,10 @@ export const EasJsonOnlyFlag = {
|
||||
};
|
||||
|
||||
export const EasUpdateEnvironmentFlag = {
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment | null>({
|
||||
environment: Flags.string({
|
||||
description:
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution.',
|
||||
options: mapToLowercase(Object.values(EnvironmentVariableEnvironment)),
|
||||
parse: upperCaseAsync,
|
||||
'Environment to use for the server-side defined EAS environment variables during command execution, e.g. "production", "preview", "development"',
|
||||
required: false,
|
||||
default: null,
|
||||
default: undefined,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { LogSpy } from './testUtils';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
|
||||
import { installDependenciesAsync } from '../../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../../onboarding/runCommand';
|
||||
import {
|
||||
cloneTemplateAsync,
|
||||
initializeGitRepositoryAsync,
|
||||
installProjectDependenciesAsync,
|
||||
} from '../commands';
|
||||
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../onboarding/installDependencies');
|
||||
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';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'ssh',
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
|
||||
it('should clone the template project with https', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(false);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
logSpy.expectLogToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'https',
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('installProjectDependenciesAsync', () => {
|
||||
it('should install the project dependencies', async () => {
|
||||
const projectDir = '/test/project-directory';
|
||||
|
||||
jest.mocked(installDependenciesAsync).mockResolvedValue();
|
||||
jest.mocked(runCommandAsync).mockResolvedValue();
|
||||
|
||||
await installProjectDependenciesAsync(projectDir, 'npm');
|
||||
|
||||
expect(installDependenciesAsync).toHaveBeenCalledWith({
|
||||
outputLevel: 'none',
|
||||
projectDir,
|
||||
packageManager: 'npm',
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', 'expo-updates'],
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', '@expo/metro-runtime'],
|
||||
showOutput: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeGitRepositoryAsync', () => {
|
||||
it('should initialize git repository', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await initializeGitRepositoryAsync(projectDir);
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['init'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['add', '.'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
cwd: projectDir,
|
||||
showOutput: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,270 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { LogSpy } from './testUtils';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import {
|
||||
findAvailableProjectNameAsync,
|
||||
generateProjectConfigAsync,
|
||||
generateProjectNameVariations,
|
||||
getAccountChoices,
|
||||
promptForProjectAccountAsync,
|
||||
} from '../configs';
|
||||
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
|
||||
describe('configs', () => {
|
||||
let logSpy: LogSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
logSpy = new LogSpy('withInfo');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.restore();
|
||||
});
|
||||
|
||||
// Helper function to verify prompt was called with expected message
|
||||
const expectPromptToHaveMessage = (expectedMessage: string): void => {
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expectedMessage,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const mockUserInput = (inputValue: any): void => {
|
||||
jest.mocked(promptAsync).mockResolvedValue(inputValue);
|
||||
};
|
||||
|
||||
describe('generateProjectConfigAsync', () => {
|
||||
const mockOptions = {
|
||||
graphqlClient: {} as ExpoGraphqlClient,
|
||||
projectAccount: 'test-account',
|
||||
};
|
||||
|
||||
describe('when no path is provided', () => {
|
||||
it('should generate name and directory when base name is available', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'new-expo-project',
|
||||
projectDirectory: expect.stringContaining('/new-expo-project'),
|
||||
});
|
||||
logSpy.expectLogToContain('Using project name: new-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
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(/^new-expo-project-jester-\d{4}-\d{2}-\d{2}$/),
|
||||
projectDirectory: expect.stringContaining(result.projectName),
|
||||
});
|
||||
logSpy.expectLogToContain('Using alternate project 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
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await generateProjectConfigAsync(jester, undefined, mockOptions);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: expect.stringMatching(
|
||||
/^new-expo-project-jester-\d{4}-\d{2}-\d{2}-[a-zA-Z0-9_-]{6}$/
|
||||
),
|
||||
projectDirectory: expect.stringContaining(result.projectName),
|
||||
});
|
||||
logSpy.expectLogToContain('Using alternate project name:');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when path is provided', () => {
|
||||
it('should use absolute path and extract name from basename', async () => {
|
||||
(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);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-project',
|
||||
projectDirectory: absolutePath,
|
||||
});
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
|
||||
it('should resolve relative path and extract name from basename', async () => {
|
||||
(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);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'my-app',
|
||||
projectDirectory: expect.stringContaining('some/relative/my-app'),
|
||||
});
|
||||
logSpy.expectLogToContain('Using project directory:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('promptForProjectAccountAsync', () => {
|
||||
it('should prompt for a project account when there are multiple accounts', async () => {
|
||||
const selectedAccount = 'jester';
|
||||
mockUserInput({ account: { name: selectedAccount } });
|
||||
|
||||
const result = await promptForProjectAccountAsync(jester);
|
||||
|
||||
expect(result).toBe(selectedAccount);
|
||||
expectPromptToHaveMessage('Which account should own this project?');
|
||||
});
|
||||
|
||||
it('should automatically select single account with permissions', async () => {
|
||||
const result = await promptForProjectAccountAsync(jester);
|
||||
expect(result).toBe(jester.accounts[0].name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccountChoices', () => {
|
||||
it('should return mapped account choices', () => {
|
||||
const result = getAccountChoices(jester);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
title: 'jester (personal account)',
|
||||
value: { name: 'jester' },
|
||||
},
|
||||
{
|
||||
title: 'other',
|
||||
value: { name: 'other' },
|
||||
disabled: true,
|
||||
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}$/),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAvailableProjectNameAsync', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const mockOptions = {
|
||||
graphqlClient: mockGraphqlClient,
|
||||
projectAccount: 'jester',
|
||||
};
|
||||
|
||||
it('should find first available name when checking both local and remote', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'test-project',
|
||||
'/base/path',
|
||||
mockOptions
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectName: 'test-project',
|
||||
projectDirectory: '/base/path/test-project',
|
||||
});
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(1);
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester',
|
||||
'test-project'
|
||||
);
|
||||
});
|
||||
|
||||
it('should try alternatives when first name exists locally', async () => {
|
||||
(fs.pathExists as jest.Mock)
|
||||
.mockResolvedValueOnce(true) // First exists
|
||||
.mockResolvedValueOnce(false); // Second available
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'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-'),
|
||||
});
|
||||
expect(fs.pathExists).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should skip name if local is available but remote exists', async () => {
|
||||
(fs.pathExists as jest.Mock).mockResolvedValue(false);
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockResolvedValueOnce('project-id') // First name exists remotely
|
||||
.mockResolvedValueOnce(null); // Second available remotely
|
||||
|
||||
const result = await findAvailableProjectNameAsync(
|
||||
jester,
|
||||
'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-'),
|
||||
});
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should throw error when all variations taken', 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');
|
||||
});
|
||||
|
||||
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:');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,385 @@
|
||||
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';
|
||||
import {
|
||||
cleanAndPrefix,
|
||||
copyProjectTemplatesAsync,
|
||||
generateAppConfigAsync,
|
||||
generateEasConfigAsync,
|
||||
updatePackageJsonAsync,
|
||||
updateReadmeAsync,
|
||||
} from '../projectFiles';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('fs-extra');
|
||||
|
||||
describe('projectFiles', () => {
|
||||
let logSpy: LogSpy;
|
||||
|
||||
beforeAll(() => {
|
||||
logSpy = new LogSpy('withTick');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readJson).mockImplementation(() => Promise.resolve({}));
|
||||
jest.mocked(fs.writeJson).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
logSpy.restore();
|
||||
});
|
||||
|
||||
describe('cleanAndPrefix', () => {
|
||||
it('should clean and prefix the string', () => {
|
||||
expect(cleanAndPrefix('test-app-123', 'app')).toBe('testapp123');
|
||||
expect(cleanAndPrefix('test@app#with$symbols', 'app')).toBe('testappwithsymbols');
|
||||
expect(cleanAndPrefix('test-app-with-dashes', 'app')).toBe('testappwithdashes');
|
||||
expect(cleanAndPrefix('TestApp', 'app')).toBe('appTestApp');
|
||||
expect(cleanAndPrefix('100 App', 'app')).toBe('app100App');
|
||||
});
|
||||
|
||||
it('should handle schemes', () => {
|
||||
expect(cleanAndPrefix('test-app-123', 'scheme')).toBe('test-app-123');
|
||||
expect(cleanAndPrefix('test@app#with$symbols', 'scheme')).toBe('testappwithsymbols');
|
||||
expect(cleanAndPrefix('test+app.with-allowed.symbols', 'scheme')).toBe(
|
||||
'test+app.with-allowed.symbols'
|
||||
);
|
||||
expect(cleanAndPrefix('100 App', 'scheme')).toBe('scheme100app');
|
||||
expect(cleanAndPrefix('TestApp', 'scheme')).toBe('testapp');
|
||||
expect(cleanAndPrefix('testApp2', 'scheme')).toBe('testapp2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAppConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
const mockUpdateUrl = 'https://u.expo.dev/test-project-id';
|
||||
|
||||
const createMockApp = (overrides: Partial<AppFragment> = {}): AppFragment => ({
|
||||
id: 'test-project-id',
|
||||
name: 'Test App',
|
||||
fullName: '@testowner/test-app',
|
||||
slug: 'test-app',
|
||||
ownerAccount: {
|
||||
id: 'testowner-account-id',
|
||||
name: 'testowner',
|
||||
users: [],
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(getEASUpdateURL).mockReturnValue(mockUpdateUrl);
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {} as ExpoConfig,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate the app config', async () => {
|
||||
const mockApp = createMockApp();
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {
|
||||
name: 'value-to-override',
|
||||
slug: 'value-to-override',
|
||||
icon: 'value-to-keep',
|
||||
} as ExpoConfig,
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(getEASUpdateURL).toHaveBeenCalledWith('test-project-id', null);
|
||||
|
||||
const expectedConfig = {
|
||||
expo: {
|
||||
name: 'Test App',
|
||||
slug: 'test-app',
|
||||
scheme: 'testapp',
|
||||
icon: 'value-to-keep',
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: 'test-project-id',
|
||||
},
|
||||
},
|
||||
owner: 'testowner',
|
||||
updates: {
|
||||
url: mockUpdateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier: 'com.testowner.testapp',
|
||||
},
|
||||
android: {
|
||||
package: 'com.testowner.testapp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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 () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test-app-with-dashes',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-account-id',
|
||||
name: 'test-owner',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithdashes',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the owner account name', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-123-account-id',
|
||||
name: 'test-owner-123',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner123.testapp',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the slug', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test@app#with$symbols',
|
||||
fullName: '@testowner/test@app#with$symbols',
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithsymbols',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateEasConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
it('should generate the eas config', async () => {
|
||||
const expectedEasConfig = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: 'remote',
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: { simulator: true },
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
await generateEasConfigAsync(projectDir);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
logSpy.expectLogToContain('Generated eas.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePackageJsonAsync', () => {
|
||||
it('should update the package.json', async () => {
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const projectDir = '/test/project-dir';
|
||||
await updatePackageJsonAsync(projectDir);
|
||||
|
||||
const expectedPackageJson = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
preview: 'npx eas-cli@latest workflow:run publish-preview-update.yml',
|
||||
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
|
||||
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
|
||||
},
|
||||
};
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/package.json`, expectedPackageJson, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
logSpy.expectLogToContain('Updated package.json with scripts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyProjectTemplatesAsync', () => {
|
||||
it('should copy the project templates', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
expect(fs.copy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('templates/.eas/workflows'),
|
||||
expect.stringContaining('.eas/workflows'),
|
||||
{ errorOnExist: false, overwrite: true }
|
||||
);
|
||||
|
||||
logSpy.expectLogToContain('Created EAS workflow files');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateReadmeAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
const readmeTemplatePath = expect.stringContaining('templates/readme-additions.md');
|
||||
const projectReadmePath = `${projectDir}/README.md`;
|
||||
|
||||
const mockReadmeTemplate = `## Get started
|
||||
|
||||
To start the app, in your terminal run:
|
||||
|
||||
\`\`\`bash
|
||||
npm run start
|
||||
\`\`\`
|
||||
|
||||
## Workflows
|
||||
`;
|
||||
const mockExistingReadme = `# My App
|
||||
|
||||
## Get started
|
||||
|
||||
Follow these steps to get started with the app.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the dependencies first.`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(fs.readFile).mockImplementation((filePath: any) => {
|
||||
const content = filePath.includes('readme-additions.md')
|
||||
? mockReadmeTemplate
|
||||
: filePath === projectReadmePath
|
||||
? mockExistingReadme
|
||||
: '';
|
||||
return Promise.resolve(content);
|
||||
});
|
||||
});
|
||||
|
||||
it('should read the existing readme and merge the template content', async () => {
|
||||
await updateReadmeAsync(projectDir, 'npm');
|
||||
|
||||
expect(fs.readFile).toHaveBeenCalledWith(readmeTemplatePath, 'utf8');
|
||||
expect(fs.readFile).toHaveBeenCalledWith(projectReadmePath, 'utf8');
|
||||
|
||||
const headings = ['# My App', '## Get started', '## Workflows'];
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
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 () => {
|
||||
await updateReadmeAsync(projectDir, 'yarn');
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
projectReadmePath,
|
||||
expect.stringContaining('yarn run start')
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle the case when 'Get started' section does not exist", async () => {
|
||||
const readmeWithoutGetStarted = `# My App
|
||||
|
||||
This is my awesome app.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the dependencies first.`;
|
||||
|
||||
jest.mocked(fs.readFile).mockImplementation((filePath: any) => {
|
||||
const content = filePath.includes('readme-additions.md')
|
||||
? mockReadmeTemplate
|
||||
: filePath === projectReadmePath
|
||||
? readmeWithoutGetStarted
|
||||
: '';
|
||||
return Promise.resolve(content);
|
||||
});
|
||||
|
||||
await updateReadmeAsync(projectDir, 'npm');
|
||||
|
||||
const headings = ['# My App', '## Installation', '## Get started', '## Workflows'];
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(
|
||||
projectReadmePath,
|
||||
expect.stringMatching(new RegExp(headings.join('[\\s\\S]*')))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import Log from '../../../log';
|
||||
|
||||
export class LogSpy {
|
||||
private readonly logSpy: jest.SpyInstance;
|
||||
|
||||
constructor(logMethod: 'warn' | 'log' | 'withTick' | 'withInfo') {
|
||||
this.logSpy = jest.spyOn(Log, logMethod);
|
||||
}
|
||||
|
||||
getLogOutput(): string[] {
|
||||
return this.logSpy.mock.calls.map(call => (call.length === 0 ? '' : call.join(' ')));
|
||||
}
|
||||
|
||||
expectLogToContain(message: string): void {
|
||||
const output = this.getLogOutput();
|
||||
// strip out ANSI codes and special characters like the tick
|
||||
const outputWithoutAnsi = output.map(line =>
|
||||
line.replace(/\x1b\[[0-9;]*m/g, '').replace(/✔\s*/, '')
|
||||
);
|
||||
expect(outputWithoutAnsi.some(line => line.includes(message))).toBeTruthy();
|
||||
}
|
||||
|
||||
restore(): void {
|
||||
this.logSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import Log from '../../log';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
|
||||
import { PackageManager, installDependenciesAsync } from '../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../onboarding/runCommand';
|
||||
|
||||
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 cloneMethod = (await canAccessRepositoryUsingSshAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
}))
|
||||
? 'ssh'
|
||||
: 'https';
|
||||
|
||||
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
targetProjectDir,
|
||||
cloneMethod,
|
||||
});
|
||||
|
||||
return finalTargetProjectDirectory;
|
||||
}
|
||||
|
||||
export async function installProjectDependenciesAsync(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
await installDependenciesAsync({
|
||||
outputLevel: 'none',
|
||||
projectDir,
|
||||
packageManager,
|
||||
});
|
||||
|
||||
const dependencies = ['expo-updates', '@expo/metro-runtime'];
|
||||
for (const dependency of dependencies) {
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', dependency],
|
||||
showOutput: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
|
||||
await fs.remove(path.join(projectDir, '.git'));
|
||||
|
||||
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
|
||||
|
||||
for (const args of commands) {
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'git',
|
||||
args,
|
||||
showOutput: false,
|
||||
});
|
||||
Log.log();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import fs from 'fs-extra';
|
||||
import { nanoid } from 'nanoid';
|
||||
import path from 'path';
|
||||
|
||||
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 { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
function validateProjectPath(resolvedPath: string): void {
|
||||
const normalizedPath = path.normalize(resolvedPath);
|
||||
|
||||
// Check for path traversal attempts
|
||||
if (normalizedPath.includes('..')) {
|
||||
throw new Error(`Invalid project path: "${resolvedPath}". Path traversal is not allowed.`);
|
||||
}
|
||||
|
||||
// Ensure we're not trying to create a project in system directories
|
||||
const systemDirs = ['/bin', '/sbin', '/etc', '/usr', '/var', '/sys', '/proc', '/dev'];
|
||||
const isSystemDir = systemDirs.some(
|
||||
dir => normalizedPath === dir || normalizedPath.startsWith(dir + path.sep)
|
||||
);
|
||||
|
||||
if (isSystemDir) {
|
||||
throw new Error(
|
||||
`Invalid project path: "${resolvedPath}". Cannot create projects in system directories.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateProjectConfigAsync(
|
||||
actor: Actor,
|
||||
pathArg: string | undefined,
|
||||
options: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectAccount: string;
|
||||
}
|
||||
): Promise<{
|
||||
projectName: string;
|
||||
projectDirectory: string;
|
||||
}> {
|
||||
let baseName = 'new-expo-project';
|
||||
let parentDirectory = process.cwd();
|
||||
|
||||
if (pathArg) {
|
||||
const resolvedPath = path.isAbsolute(pathArg) ? pathArg : path.resolve(process.cwd(), pathArg);
|
||||
validateProjectPath(resolvedPath);
|
||||
baseName = path.basename(resolvedPath);
|
||||
parentDirectory = path.dirname(resolvedPath);
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
|
||||
return {
|
||||
projectName,
|
||||
projectDirectory,
|
||||
};
|
||||
}
|
||||
|
||||
function getAccountPermissionsMap(actor: Actor): Map<string, boolean> {
|
||||
const permissionsMap = new Map<string, boolean>();
|
||||
|
||||
for (const account of actor.accounts) {
|
||||
const hasPermission =
|
||||
account.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly;
|
||||
permissionsMap.set(account.name, hasPermission);
|
||||
}
|
||||
|
||||
return permissionsMap;
|
||||
}
|
||||
|
||||
export async function promptForProjectAccountAsync(actor: Actor): Promise<string> {
|
||||
const permissionsMap = getAccountPermissionsMap(actor);
|
||||
|
||||
// If only one account, use it (if has permissions)
|
||||
if (actor.accounts.length === 1) {
|
||||
const account = actor.accounts[0];
|
||||
|
||||
if (permissionsMap.get(account.name)) {
|
||||
return account.name;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`You don't have permission to create projects on your only available account (${account.name}).`
|
||||
);
|
||||
}
|
||||
|
||||
// Multiple accounts - prompt user to select one with permissions
|
||||
return (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices: getAccountChoices(actor, permissionsMap),
|
||||
})
|
||||
).account.name;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
return sortedAccounts.map(account => {
|
||||
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
||||
const accountDisplayName = isPersonalAccount
|
||||
? `${account.name} (personal account)`
|
||||
: account.name;
|
||||
const disabled = !permissions.get(account.name);
|
||||
|
||||
return {
|
||||
title: accountDisplayName,
|
||||
value: { name: account.name },
|
||||
...(disabled && {
|
||||
disabled: true,
|
||||
description: 'You do not have the required permissions to create projects on this account.',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
projectName: string,
|
||||
{ silent = false }: { silent?: boolean } = {}
|
||||
): Promise<boolean> {
|
||||
const existingProjectId = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient,
|
||||
accountName,
|
||||
projectName
|
||||
);
|
||||
|
||||
const doesNotExist = existingProjectId === null;
|
||||
if (!doesNotExist && !silent) {
|
||||
Log.warn(`Project @${accountName}/${projectName} already exists on the server.`);
|
||||
}
|
||||
|
||||
return doesNotExist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an available project name that doesn't conflict with either:
|
||||
* Local filesystem (directory already exists)
|
||||
* Remote server (project already exists on Expo)
|
||||
*/
|
||||
export async function findAvailableProjectNameAsync(
|
||||
actor: Actor,
|
||||
baseName: string,
|
||||
parentDirectory: string,
|
||||
{
|
||||
graphqlClient,
|
||||
projectAccount,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectAccount: string;
|
||||
}
|
||||
): Promise<{ projectName: string; projectDirectory: string }> {
|
||||
const nameVariations = generateProjectNameVariations(actor, baseName);
|
||||
|
||||
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(proposedDirectory);
|
||||
if (localExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to find a unique project name for "${baseName}". All generated variations already exist.`
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
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';
|
||||
|
||||
// Android package names must start with a lowercase letter
|
||||
// schemes must start with a lowercase letter and can only contain lowercase letters, digits, "+", "." or "-"
|
||||
export function cleanAndPrefix(_string: string, type: 'user' | 'app' | 'scheme'): string {
|
||||
let string = _string;
|
||||
let pattern = /[^A-Za-z0-9]/g;
|
||||
if (type === 'scheme') {
|
||||
string = _string.toLowerCase();
|
||||
pattern = /[^a-z0-9+.-]/g;
|
||||
}
|
||||
|
||||
const prefix = /^[^a-z]/.test(string) ? type : '';
|
||||
const cleaned = string.replaceAll(pattern, '');
|
||||
|
||||
return prefix + cleaned;
|
||||
}
|
||||
|
||||
export async function generateAppConfigAsync(projectDir: string, app: AppFragment): Promise<void> {
|
||||
const user = cleanAndPrefix(app.ownerAccount.name, 'user');
|
||||
const slug = cleanAndPrefix(app.slug, 'app');
|
||||
const scheme = cleanAndPrefix(app.name ?? app.slug, 'scheme');
|
||||
|
||||
const bundleIdentifier = `com.${user}.${slug}`;
|
||||
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
|
||||
|
||||
const { expo: baseExpoConfig } = await fs.readJson(path.join(projectDir, 'app.json'));
|
||||
const expoConfig: ExpoConfig = {
|
||||
name: app.name ?? app.slug,
|
||||
slug: app.slug,
|
||||
scheme,
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: app.id,
|
||||
},
|
||||
},
|
||||
owner: app.ownerAccount.name,
|
||||
updates: {
|
||||
url: updateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier,
|
||||
},
|
||||
android: {
|
||||
package: bundleIdentifier,
|
||||
},
|
||||
};
|
||||
const mergedConfig = merge(baseExpoConfig, expoConfig);
|
||||
|
||||
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> {
|
||||
const easBuildGitHubConfig = {
|
||||
android: {
|
||||
image: 'latest',
|
||||
},
|
||||
ios: {
|
||||
image: 'latest',
|
||||
},
|
||||
};
|
||||
|
||||
const easJson: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: {
|
||||
simulator: true,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
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> {
|
||||
const packageJsonPath = path.join(projectDir, 'package.json');
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.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');
|
||||
|
||||
await fs.copy(templatesSourceDir, easWorkflowsTargetDir, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
});
|
||||
|
||||
Log.withTick('Created EAS workflow files');
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function updateReadmeAsync(
|
||||
projectDir: string,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
const readmeTemplatePath = path.join(__dirname, 'templates', 'readme-additions.md');
|
||||
const projectReadmePath = path.join(projectDir, 'README.md');
|
||||
|
||||
const readmeAdditions = await fs.readFile(readmeTemplatePath, 'utf8');
|
||||
const existingReadme = await fs.readFile(projectReadmePath, 'utf8');
|
||||
|
||||
const targetSection = '## Get started';
|
||||
const sectionIndex = existingReadme.indexOf(targetSection);
|
||||
|
||||
let mergedReadme: string;
|
||||
if (sectionIndex !== -1) {
|
||||
// Find the next ## section after "## Get started"
|
||||
const afterTargetSection = existingReadme.substring(sectionIndex);
|
||||
const nextSectionMatch = afterTargetSection.match(/\n## /);
|
||||
let endIndex = existingReadme.length;
|
||||
if (nextSectionMatch?.index !== undefined) {
|
||||
// Replace from "## Get started" to the next "##" section
|
||||
endIndex = sectionIndex + nextSectionMatch.index;
|
||||
}
|
||||
|
||||
const beforeSection = existingReadme.substring(0, sectionIndex).trim();
|
||||
const afterSection = existingReadme.substring(endIndex);
|
||||
mergedReadme = beforeSection + '\n\n' + readmeAdditions.trim() + '\n\n' + afterSection;
|
||||
} else {
|
||||
// No "Get started" section found, append the template to the existing README
|
||||
mergedReadme = existingReadme.trim() + '\n\n' + readmeAdditions.trim();
|
||||
}
|
||||
|
||||
mergedReadme = mergedReadme.replaceAll('npm run', `${packageManager} run`);
|
||||
|
||||
await fs.writeFile(projectReadmePath, mergedReadme);
|
||||
|
||||
Log.withTick('Updated README.md with EAS configuration details');
|
||||
Log.log();
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+17
@@ -1,3 +1,20 @@
|
||||
## Get started
|
||||
|
||||
To start the app, in your terminal run:
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in:
|
||||
|
||||
- [a development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [an Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [an iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Workflows
|
||||
|
||||
This project is configured to use [EAS Workflows](https://docs.expo.dev/eas/workflows/get-started/) to automate some development and release processes. These commands are set up in [`package.json`](./package.json) and can be run using NPM scripts in your terminal.
|
||||
@@ -1,8 +1,12 @@
|
||||
import { WorkflowJobResult } from './types';
|
||||
import { BuildQuery } from '../../graphql/queries/BuildQuery';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
// This function is in a separate module for testing purposes
|
||||
|
||||
export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<string | null> {
|
||||
export async function fetchRawLogsForCustomJobAsync(
|
||||
job: WorkflowJobResult
|
||||
): Promise<string | null> {
|
||||
const firstLogFileUrl = job.turtleJobRun?.logFileUrls?.[0];
|
||||
if (!firstLogFileUrl) {
|
||||
return null;
|
||||
@@ -13,3 +17,25 @@ export async function fetchRawLogsForJobAsync(job: WorkflowJobResult): Promise<s
|
||||
const rawLogs = await response.text();
|
||||
return rawLogs;
|
||||
}
|
||||
|
||||
export async function fetchRawLogsForBuildJobAsync(
|
||||
state: { graphqlClient: ExpoGraphqlClient },
|
||||
job: WorkflowJobResult
|
||||
): Promise<string | null> {
|
||||
const buildId = job.outputs?.build_id;
|
||||
if (!buildId) {
|
||||
return null;
|
||||
}
|
||||
const buildFragment = await BuildQuery.byIdAsync(state.graphqlClient, buildId, {
|
||||
useCache: false,
|
||||
});
|
||||
const firstLogFileUrl = buildFragment.logFiles?.[0];
|
||||
if (!firstLogFileUrl) {
|
||||
return null;
|
||||
}
|
||||
const response = await fetch(firstLogFileUrl, {
|
||||
method: 'GET',
|
||||
});
|
||||
const rawLogs = await response.text();
|
||||
return rawLogs;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DefaultEnvironment } from '../../build/utils/environment';
|
||||
import { booleanLike, stringLike } from '../../credentials/ios/types';
|
||||
import Log from '../../log';
|
||||
import { promptAsync } from '../../prompts';
|
||||
|
||||
const inputExtraProperties = {
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
required: booleanLike.default(false).optional().describe('Whether the input is required.'),
|
||||
};
|
||||
|
||||
// Adapted from the input definition on the server (https://github.com/expo/universe/pull/21950)
|
||||
export const WorkflowDispatchInputZ = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('string').default('string').optional(),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('boolean'),
|
||||
default: booleanLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('choice'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
|
||||
}),
|
||||
z.object({
|
||||
...inputExtraProperties,
|
||||
type: z.literal('environment'),
|
||||
default: z
|
||||
.enum(Object.values(DefaultEnvironment))
|
||||
.optional()
|
||||
.describe('Default value for the input'),
|
||||
}),
|
||||
]);
|
||||
export function parseInputs(inputFlags: string[]): Record<string, string> {
|
||||
const inputs: Record<string, string> = {};
|
||||
|
||||
for (const inputFlag of inputFlags) {
|
||||
const equalIndex = inputFlag.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
|
||||
}
|
||||
|
||||
const key = inputFlag.substring(0, equalIndex);
|
||||
const value = inputFlag.substring(equalIndex + 1);
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
|
||||
}
|
||||
|
||||
inputs[key] = value;
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export function parseJsonInputs(jsonString: string): Record<string, string> {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('JSON input must be an object.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input.`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
.parse(parsed?.on?.workflow_dispatch?.inputs);
|
||||
} catch (error) {
|
||||
Log.warn('Failed to parse workflow inputs from YAML:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
export async function maybePromptForMissingInputsAsync({
|
||||
inputSpecs,
|
||||
inputs,
|
||||
}: {
|
||||
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
|
||||
inputs: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
|
||||
|
||||
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
|
||||
|
||||
if (missingRequiredInputs.length === 0) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Some required inputs are missing. Please provide them:');
|
||||
|
||||
const nextInputs = { ...inputs };
|
||||
|
||||
for (const [key, spec] of missingRequiredInputs) {
|
||||
const value = await promptForMissingInputAsync({ key, spec });
|
||||
nextInputs[key] = value;
|
||||
}
|
||||
|
||||
return nextInputs;
|
||||
}
|
||||
|
||||
async function promptForMissingInputAsync({
|
||||
key,
|
||||
spec,
|
||||
}: {
|
||||
key: string;
|
||||
spec: z.infer<typeof WorkflowDispatchInputZ>;
|
||||
}): Promise<unknown> {
|
||||
const message = spec.description ? `${key} (${spec.description})` : key;
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'number',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: number) => {
|
||||
if (isNaN(val)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'choice': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'value',
|
||||
message,
|
||||
choices: spec.options.map(option => ({
|
||||
title: option,
|
||||
value: option,
|
||||
})),
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'environment':
|
||||
default: {
|
||||
const { value } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: string) => {
|
||||
if (spec.required && (!val || val.trim() === '')) {
|
||||
return 'This field is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
choiceFromWorkflowJob,
|
||||
choiceFromWorkflowRun,
|
||||
choicesFromWorkflowLogs,
|
||||
processLogsFromJobAsync,
|
||||
fetchAndProcessLogsFromJobAsync,
|
||||
processWorkflowRuns,
|
||||
} from './utils';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
@@ -268,7 +268,7 @@ export const workflowStepSelectionAction: WorkflowCommandSelectionAction = async
|
||||
if (!job) {
|
||||
return moveToWorkflowSelectionErrorState(prevState, 'No job found');
|
||||
}
|
||||
const logs = await processLogsFromJobAsync(job);
|
||||
const logs = await fetchAndProcessLogsFromJobAsync(prevState, job);
|
||||
if (!logs) {
|
||||
return moveToWorkflowSelectionErrorState(prevState, 'No logs found');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { WorkflowJobByIdQuery, WorkflowRunByIdWithJobsQuery } from '../../graphql/generated';
|
||||
|
||||
/*
|
||||
* Utility types for workflow commands
|
||||
* Utility types and Zod definitions for workflow commands
|
||||
*/
|
||||
|
||||
export enum WorkflowTriggerType {
|
||||
@@ -42,5 +42,12 @@ export type WorkflowRunWithJobsResult = WorkflowRunResult & {
|
||||
logs?: string;
|
||||
};
|
||||
|
||||
export type WorkflowLogLine = { time: string; msg: string; result?: string; marker?: string };
|
||||
export type WorkflowLogLine = {
|
||||
time: string;
|
||||
msg: string;
|
||||
result?: string;
|
||||
marker?: string;
|
||||
err?: any;
|
||||
};
|
||||
|
||||
export type WorkflowLogs = Map<string, WorkflowLogLine[]>;
|
||||
@@ -1,4 +1,6 @@
|
||||
import { fetchRawLogsForJobAsync } from './fetchLogs';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
import { fetchRawLogsForBuildJobAsync, fetchRawLogsForCustomJobAsync } from './fetchLogs';
|
||||
import {
|
||||
WorkflowJobResult,
|
||||
WorkflowLogLine,
|
||||
@@ -7,11 +9,17 @@ import {
|
||||
WorkflowTriggerType,
|
||||
} from './types';
|
||||
import {
|
||||
WorkflowJobStatus,
|
||||
WorkflowJobType,
|
||||
WorkflowRunByIdWithJobsQuery,
|
||||
WorkflowRunFragment,
|
||||
WorkflowRunStatus,
|
||||
WorkflowRunTriggerEventType,
|
||||
} from '../../graphql/generated';
|
||||
import Log from '../../log';
|
||||
import { Choice } from '../../prompts';
|
||||
import formatFields from '../../utils/formatFields';
|
||||
import { ExpoGraphqlClient } from '../context/contextUtils/createGraphqlClient';
|
||||
|
||||
export function computeTriggerInfoForWorkflowRun(run: WorkflowRunFragment): {
|
||||
triggerType: WorkflowTriggerType;
|
||||
@@ -68,16 +76,23 @@ export function choiceFromWorkflowJob(job: WorkflowJobResult, index: number): Ch
|
||||
};
|
||||
}
|
||||
|
||||
export function choicesFromWorkflowLogs(logs: WorkflowLogs): Choice[] {
|
||||
return Array.from(logs.keys()).map(step => {
|
||||
const logLines = logs.get(step);
|
||||
const stepStatus =
|
||||
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
|
||||
return {
|
||||
title: `${step} - ${stepStatus}`,
|
||||
value: step,
|
||||
};
|
||||
});
|
||||
export function choicesFromWorkflowLogs(
|
||||
logs: WorkflowLogs
|
||||
): (Choice & { name: string; status: string; logLines: WorkflowLogLine[] | undefined })[] {
|
||||
return Array.from(logs.keys())
|
||||
.map(step => {
|
||||
const logLines = logs.get(step);
|
||||
const stepStatus =
|
||||
logLines?.filter((line: WorkflowLogLine) => line.marker === 'end-step')[0]?.result ?? '';
|
||||
return {
|
||||
title: `${step} - ${stepStatus}`,
|
||||
name: step,
|
||||
status: stepStatus,
|
||||
value: step,
|
||||
logLines,
|
||||
};
|
||||
})
|
||||
.filter(step => step.status !== 'skipped');
|
||||
}
|
||||
|
||||
export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunResult[] {
|
||||
@@ -100,29 +115,171 @@ export function processWorkflowRuns(runs: WorkflowRunFragment[]): WorkflowRunRes
|
||||
});
|
||||
}
|
||||
|
||||
export async function processLogsFromJobAsync(
|
||||
export async function fetchAndProcessLogsFromJobAsync(
|
||||
state: { graphqlClient: ExpoGraphqlClient },
|
||||
job: WorkflowJobResult
|
||||
): Promise<WorkflowLogs | null> {
|
||||
const rawLogs = await fetchRawLogsForJobAsync(job);
|
||||
let rawLogs: string | null;
|
||||
switch (job.type) {
|
||||
case WorkflowJobType.Build:
|
||||
case WorkflowJobType.Repack:
|
||||
rawLogs = await fetchRawLogsForBuildJobAsync(state, job);
|
||||
break;
|
||||
default:
|
||||
rawLogs = await fetchRawLogsForCustomJobAsync(job);
|
||||
break;
|
||||
}
|
||||
if (!rawLogs) {
|
||||
return null;
|
||||
}
|
||||
Log.debug(`rawLogs = ${JSON.stringify(rawLogs, null, 2)}`);
|
||||
const logs: WorkflowLogs = new Map();
|
||||
const logKeys = new Set<string>();
|
||||
rawLogs.split('\n').forEach(line => {
|
||||
rawLogs.split('\n').forEach((line, index) => {
|
||||
Log.debug(`line ${index} = ${JSON.stringify(line, null, 2)}`);
|
||||
try {
|
||||
const parsedLine = JSON.parse(line);
|
||||
const { result, marker } = parsedLine;
|
||||
const { buildStepDisplayName, buildStepInternalId, time, msg } = parsedLine;
|
||||
const { buildStepDisplayName, buildStepInternalId, time, msg, result, marker, err } =
|
||||
parsedLine;
|
||||
const stepId = buildStepDisplayName ?? buildStepInternalId;
|
||||
if (stepId) {
|
||||
if (!logKeys.has(stepId)) {
|
||||
logKeys.add(stepId);
|
||||
logs.set(stepId, []);
|
||||
}
|
||||
logs.get(stepId)?.push({ time, msg, result, marker });
|
||||
logs.get(stepId)?.push({ time, msg, result, marker, err });
|
||||
}
|
||||
} catch {}
|
||||
});
|
||||
return logs;
|
||||
}
|
||||
|
||||
function descriptionForJobStatus(status: WorkflowJobStatus): string {
|
||||
switch (status) {
|
||||
case WorkflowJobStatus.New:
|
||||
return 'Waiting for worker';
|
||||
case WorkflowJobStatus.InProgress:
|
||||
return 'In progress';
|
||||
case WorkflowJobStatus.Success:
|
||||
return 'Completed successfully';
|
||||
case WorkflowJobStatus.Failure:
|
||||
return 'Failed';
|
||||
case WorkflowJobStatus.Canceled:
|
||||
return 'Canceled';
|
||||
case WorkflowJobStatus.Skipped:
|
||||
return 'Skipped';
|
||||
case WorkflowJobStatus.ActionRequired:
|
||||
return 'Waiting for action';
|
||||
case WorkflowJobStatus.PendingCancel:
|
||||
return 'Pending cancel';
|
||||
}
|
||||
}
|
||||
|
||||
export async function infoForActiveWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId'],
|
||||
maxLogLines: number = 5 // -1 means no limit
|
||||
): Promise<string> {
|
||||
const statusLines = [];
|
||||
const statusValues = [];
|
||||
for (const job of workflowRun.jobs) {
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusValues.push({ label: ' Job', value: job.name });
|
||||
statusValues.push({ label: ' Status', value: descriptionForJobStatus(job.status) });
|
||||
if (job.status !== WorkflowJobStatus.InProgress) {
|
||||
continue;
|
||||
}
|
||||
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
|
||||
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
|
||||
if (steps.length > 0) {
|
||||
const currentStep = steps[steps.length - 1];
|
||||
statusValues.push({ label: ' Current step', value: currentStep.name });
|
||||
if (currentStep?.logLines?.length) {
|
||||
statusValues.push({ label: ' Current logs', value: '' });
|
||||
const currentLogs =
|
||||
currentStep.logLines
|
||||
?.map(line => line.msg)
|
||||
.filter((_, index) => {
|
||||
if (maxLogLines === -1) {
|
||||
return true;
|
||||
}
|
||||
return index > (currentStep.logLines?.length ?? 0) - maxLogLines;
|
||||
}) ?? [];
|
||||
for (const log of currentLogs) {
|
||||
statusValues.push({ label: '', value: log });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusLines.push(formatFields(statusValues));
|
||||
return statusLines.join('\n');
|
||||
}
|
||||
|
||||
export async function infoForFailedWorkflowRunAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
workflowRun: WorkflowRunByIdWithJobsQuery['workflowRuns']['byId']
|
||||
): Promise<string> {
|
||||
const statusLines = [];
|
||||
const statusValues = [];
|
||||
for (const job of workflowRun.jobs) {
|
||||
if (job.status !== WorkflowJobStatus.Failure) {
|
||||
continue;
|
||||
}
|
||||
const logs = await fetchAndProcessLogsFromJobAsync({ graphqlClient }, job);
|
||||
const steps = logs ? choicesFromWorkflowLogs(logs) : [];
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusValues.push({ label: ' Failed job', value: job.name });
|
||||
if (steps.length > 0) {
|
||||
const failedStep = steps.find(step => step.status === 'fail');
|
||||
if (failedStep) {
|
||||
const logs = failedStep.logLines?.map(line => line.msg) ?? [];
|
||||
statusValues.push({ label: ' Failed step', value: failedStep.name });
|
||||
statusValues.push({
|
||||
label: ' Logs for failed step',
|
||||
value: '',
|
||||
});
|
||||
for (const log of logs) {
|
||||
statusValues.push({ label: '', value: log });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
statusValues.push({ label: '', value: '' });
|
||||
statusLines.push(formatFields(statusValues));
|
||||
return statusLines.join('\n');
|
||||
}
|
||||
export async function fileExistsAsync(filePath: string): Promise<boolean> {
|
||||
return await fs.promises
|
||||
.access(filePath, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
export async function maybeReadStdinAsync(): Promise<string | null> {
|
||||
// Check if there's data on stdin
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const trimmedData = data.trim();
|
||||
resolve(trimmedData || null);
|
||||
});
|
||||
|
||||
process.stdin.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEn
|
||||
import { ensureIosCredentialsForBuildResignAsync } from '../../build/ios/credentials';
|
||||
import { prepareCredentialsToResign } from '../../build/ios/prepareJob';
|
||||
import { listAndSelectBuildOnAppAsync } from '../../build/queries';
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import { printBuildResults, printLogsUrls } from '../../build/utils/printBuildInfo';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
@@ -48,7 +47,7 @@ interface BuildResignFlags {
|
||||
sourceProfile?: string;
|
||||
maybeBuildId?: string;
|
||||
wait: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
}
|
||||
interface RawBuildResignFlags {
|
||||
json: boolean;
|
||||
@@ -60,7 +59,7 @@ interface RawBuildResignFlags {
|
||||
'source-profile': string | undefined;
|
||||
wait: boolean;
|
||||
id: string | undefined;
|
||||
environment: EnvironmentVariableEnvironment | undefined;
|
||||
environment: string | undefined;
|
||||
}
|
||||
|
||||
export default class BuildResign extends EasCommand {
|
||||
|
||||
@@ -5,7 +5,6 @@ import chalk from 'chalk';
|
||||
import fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import { getHostingDeploymentsUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASEnvironmentFlag, EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
|
||||
@@ -45,7 +44,7 @@ interface DeployFlags {
|
||||
json: boolean;
|
||||
isProduction: boolean;
|
||||
aliasName?: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
deploymentIdentifier?: string;
|
||||
exportDir: string;
|
||||
dryRun: boolean;
|
||||
@@ -53,7 +52,7 @@ interface DeployFlags {
|
||||
|
||||
interface RawDeployFlags {
|
||||
'non-interactive': boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
json: boolean;
|
||||
prod: boolean;
|
||||
alias?: string;
|
||||
|
||||
+12
-15
@@ -1,7 +1,7 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -103,7 +103,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -140,7 +140,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
},
|
||||
]);
|
||||
@@ -168,7 +168,7 @@ describe(EnvCreate, () => {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Secret,
|
||||
});
|
||||
});
|
||||
@@ -201,7 +201,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -236,7 +236,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: 'otherId',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
@@ -274,7 +274,7 @@ describe(EnvCreate, () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
id: otherVariableId,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
},
|
||||
]);
|
||||
@@ -285,7 +285,7 @@ describe(EnvCreate, () => {
|
||||
id: otherVariableId,
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
});
|
||||
@@ -320,10 +320,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [
|
||||
EnvironmentVariableEnvironment.Production,
|
||||
EnvironmentVariableEnvironment.Development,
|
||||
],
|
||||
environments: [DefaultEnvironment.Production, DefaultEnvironment.Development],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -343,7 +340,7 @@ describe(EnvCreate, () => {
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
// @ts-expect-error
|
||||
.mockImplementation(async () => [EnvironmentVariableEnvironment.Production]);
|
||||
.mockImplementation(async () => [DefaultEnvironment.Production]);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -361,7 +358,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'VarName',
|
||||
value: 'VarValue',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
@@ -400,7 +397,7 @@ describe(EnvCreate, () => {
|
||||
{
|
||||
name: 'TEST_VAR',
|
||||
value: 'test-value',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
@@ -37,7 +37,7 @@ describe(EnvDelete, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -129,7 +129,7 @@ describe(EnvDelete, () => {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -156,7 +156,7 @@ describe(EnvDelete, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
environment: DefaultEnvironment.Development,
|
||||
});
|
||||
});
|
||||
});
|
||||
+13
-7
@@ -1,6 +1,6 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -24,7 +24,7 @@ describe(EnvExec, () => {
|
||||
id: 'var1',
|
||||
name: 'EXPO_PUBLIC_API_URL',
|
||||
value: 'https://api.example.com',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -58,13 +58,13 @@ describe(EnvExec, () => {
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
environment: DefaultEnvironment.Development,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid environment when using positional argument', async () => {
|
||||
const command = new EnvExec(['invalid', 'echo test'], mockConfig);
|
||||
it('accepts custom environment when using positional argument', async () => {
|
||||
const command = new EnvExec(['custom-environment', 'echo $EXPO_PUBLIC_API_URL'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -72,8 +72,14 @@ describe(EnvExec, () => {
|
||||
projectId: testProjectId,
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -27,7 +27,7 @@ describe(EnvGet, () => {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -82,7 +82,7 @@ describe(EnvGet, () => {
|
||||
it('prompts for variable name and environment if the name is ambigous', async () => {
|
||||
jest
|
||||
.mocked(promptVariableEnvironmentAsync)
|
||||
.mockResolvedValueOnce(EnvironmentVariableEnvironment.Production);
|
||||
.mockResolvedValueOnce(DefaultEnvironment.Production);
|
||||
jest.mocked(promptVariableNameAsync).mockResolvedValueOnce('TEST_VAR_1');
|
||||
|
||||
const command = new EnvGet([], mockConfig);
|
||||
@@ -98,7 +98,7 @@ describe(EnvGet, () => {
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -135,7 +135,7 @@ describe(EnvGet, () => {
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
includeFileContent: true,
|
||||
}
|
||||
|
||||
+23
-29
@@ -1,7 +1,7 @@
|
||||
import { Config } from '@oclif/core';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -13,10 +13,12 @@ import {
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptVariableEnvironmentAsync } from '../../../utils/prompts';
|
||||
import EnvList from '../list';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../utils/prompts');
|
||||
jest.mock('../../../log');
|
||||
|
||||
const mockVariables: EnvironmentVariableFragment[] = [
|
||||
@@ -24,7 +26,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
@@ -35,7 +37,7 @@ const mockVariables: EnvironmentVariableFragment[] = [
|
||||
id: 'var2',
|
||||
name: 'TEST_VAR_2',
|
||||
value: 'value2',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
createdAt: undefined,
|
||||
@@ -51,27 +53,19 @@ describe(EnvList, () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
});
|
||||
|
||||
it('lists project environment variables successfully', async () => {
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValueOnce(mockVariables);
|
||||
|
||||
const command = new EnvList([], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
});
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_2'));
|
||||
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
|
||||
async (args: any) => {
|
||||
if (args.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
}
|
||||
if (args.multiple) {
|
||||
return [DefaultEnvironment.Production];
|
||||
}
|
||||
return DefaultEnvironment.Production;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('lists project environment variables in specified environments', async () => {
|
||||
@@ -88,7 +82,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Production,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
@@ -110,7 +104,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
environment: DefaultEnvironment.Development,
|
||||
includeFileContent: false,
|
||||
});
|
||||
});
|
||||
@@ -133,7 +127,7 @@ describe(EnvList, () => {
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
}
|
||||
);
|
||||
@@ -155,7 +149,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
@@ -178,7 +172,7 @@ describe(EnvList, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.sharedWithSensitiveAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: undefined,
|
||||
environment: DefaultEnvironment.Production,
|
||||
includeFileContent: false,
|
||||
});
|
||||
expect(Log.log).toHaveBeenCalledWith(expect.stringContaining('TEST_VAR_1'));
|
||||
|
||||
+17
-10
@@ -2,7 +2,7 @@ import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -34,7 +34,7 @@ describe(EnvPull, () => {
|
||||
id: 'var1',
|
||||
name: 'EXPO_PUBLIC_API_URL',
|
||||
value: 'https://api.example.com',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -45,7 +45,7 @@ describe(EnvPull, () => {
|
||||
id: 'var2',
|
||||
name: 'DATABASE_URL',
|
||||
value: 'postgres://localhost:5432/mydb',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -56,7 +56,7 @@ describe(EnvPull, () => {
|
||||
id: 'var3',
|
||||
name: 'SECRET_KEY',
|
||||
value: 'super-secret-key',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -67,7 +67,7 @@ describe(EnvPull, () => {
|
||||
id: 'var4',
|
||||
name: 'CONFIG_FILE',
|
||||
value: 'base64-encoded-file-content',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -95,7 +95,7 @@ describe(EnvPull, () => {
|
||||
});
|
||||
|
||||
describe('environment validation', () => {
|
||||
it('accepts development environment (case insensitive)', async () => {
|
||||
it('accepts development environment', async () => {
|
||||
const command = new EnvPull(['development'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
@@ -117,8 +117,8 @@ describe(EnvPull, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects invalid environment', async () => {
|
||||
const command = new EnvPull(['invalid'], mockConfig);
|
||||
it('accepts custom environment', async () => {
|
||||
const command = new EnvPull(['custom-environment'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -127,8 +127,15 @@ describe(EnvPull, () => {
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdWithSensitiveAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
{
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
includeFileContent: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
+209
-20
@@ -1,7 +1,7 @@
|
||||
import { Config } from '@oclif/core';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { testProjectId } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import {
|
||||
@@ -12,12 +12,14 @@ import {
|
||||
import { EnvironmentVariableMutation } from '../../../graphql/mutations/EnvironmentVariableMutation';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { confirmAsync } from '../../../prompts';
|
||||
import EnvPush from '../push';
|
||||
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('fs-extra');
|
||||
jest.mock('../../../log');
|
||||
jest.mock('../../../prompts');
|
||||
|
||||
describe(EnvPush, () => {
|
||||
const graphqlClient = {} as any as ExpoGraphqlClient;
|
||||
@@ -26,12 +28,14 @@ describe(EnvPush, () => {
|
||||
const testEnvPath = '.env.test';
|
||||
|
||||
const mockEnvContent = `EXPO_PUBLIC_API_URL=https://api.example.com
|
||||
DATABASE_URL=postgres://localhost:5432/mydb
|
||||
VARIABLE_NAME=variable value
|
||||
SECRET_KEY=super-secret-key`;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.spyOn(Log, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(Log, 'warn').mockImplementation(() => {});
|
||||
jest.spyOn(Log, 'error').mockImplementation(() => {});
|
||||
|
||||
// Mock fs-extra methods
|
||||
jest.mocked(fs.exists).mockImplementation(() => Promise.resolve(true));
|
||||
@@ -39,17 +43,10 @@ SECRET_KEY=super-secret-key`;
|
||||
|
||||
// Mock GraphQL queries and mutations
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([]); // No existing variables
|
||||
jest.mocked(EnvironmentVariableMutation.createForAppAsync).mockResolvedValue({
|
||||
id: 'var1',
|
||||
name: 'EXPO_PUBLIC_API_URL',
|
||||
value: 'https://api.example.com',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
type: EnvironmentSecretType.String,
|
||||
});
|
||||
jest
|
||||
.mocked(EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync)
|
||||
.mockResolvedValue(true);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it('accepts development environment when using positional argument', async () => {
|
||||
@@ -68,13 +65,13 @@ SECRET_KEY=super-secret-key`;
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
filterNames: ['EXPO_PUBLIC_API_URL', 'DATABASE_URL', 'SECRET_KEY'],
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects invalid environment when using positional argument', async () => {
|
||||
const command = new EnvPush(['invalid', '--path', testEnvPath], mockConfig);
|
||||
it('accepts custom environment', async () => {
|
||||
const command = new EnvPush(['custom-environment', '--path', testEnvPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
@@ -83,8 +80,200 @@ SECRET_KEY=super-secret-key`;
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await expect(command.runAsync()).rejects.toThrow(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
await command.runAsync();
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: testProjectId,
|
||||
environment: 'custom-environment',
|
||||
filterNames: ['EXPO_PUBLIC_API_URL', 'VARIABLE_NAME', 'SECRET_KEY'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('--force option', () => {
|
||||
it('automatically overrides existing variables without confirmation when --force is used', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should not call confirmAsync for existing variables
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
|
||||
// Should log that force flag is being used
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
|
||||
// Should call the mutation with overwrite flag
|
||||
expect(
|
||||
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
|
||||
).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
overwrite: true,
|
||||
}),
|
||||
]),
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
|
||||
it('automatically overrides sensitive variables without confirmation when --force is used', async () => {
|
||||
const existingSensitiveVariable = {
|
||||
id: 'var1',
|
||||
name: 'SECRET_KEY',
|
||||
value: 'old-secret-key',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest
|
||||
.mocked(EnvironmentVariablesQuery.byAppIdAsync)
|
||||
.mockResolvedValue([existingSensitiveVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should not call confirmAsync for sensitive variables
|
||||
expect(confirmAsync).not.toHaveBeenCalled();
|
||||
|
||||
// Should log that force flag is being used for sensitive variables
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
|
||||
// Should call the mutation with overwrite flag
|
||||
expect(
|
||||
EnvironmentVariableMutation.createBulkEnvironmentVariablesForAppAsync
|
||||
).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'SECRET_KEY',
|
||||
value: 'super-secret-key',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
overwrite: true,
|
||||
}),
|
||||
]),
|
||||
testProjectId
|
||||
);
|
||||
});
|
||||
|
||||
it('logs appropriate messages when using --force flag', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath, '--force'], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should log both force messages
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
expect(Log.log).toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
});
|
||||
|
||||
it('still prompts for confirmation when --force is not used', async () => {
|
||||
const existingVariable = {
|
||||
id: 'var1',
|
||||
name: 'VARIABLE_NAME',
|
||||
value: 'old variable value',
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
visibility: EnvironmentVariableVisibility.Sensitive,
|
||||
type: EnvironmentSecretType.String,
|
||||
};
|
||||
|
||||
jest.mocked(EnvironmentVariablesQuery.byAppIdAsync).mockResolvedValue([existingVariable]);
|
||||
jest.mocked(confirmAsync).mockResolvedValue(true);
|
||||
|
||||
const command = new EnvPush(['development', '--path', testEnvPath], mockConfig);
|
||||
|
||||
// @ts-expect-error
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue({
|
||||
loggedIn: { graphqlClient },
|
||||
projectId: testProjectId,
|
||||
projectDir: testProjectDir,
|
||||
});
|
||||
|
||||
await command.runAsync();
|
||||
|
||||
// Should call confirmAsync for both existing variables and sensitive variables
|
||||
expect(confirmAsync).toHaveBeenCalledTimes(2);
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message:
|
||||
'The VARIABLE_NAME environment variable already exists in development environment. Do you want to override it?',
|
||||
});
|
||||
expect(confirmAsync).toHaveBeenCalledWith({
|
||||
message:
|
||||
'You are about to overwrite sensitive variables.\n- VARIABLE_NAME\n Do you want to continue?',
|
||||
});
|
||||
|
||||
// Should not log force messages
|
||||
expect(Log.log).not.toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding existing variables.'
|
||||
);
|
||||
expect(Log.log).not.toHaveBeenCalledWith(
|
||||
'Using --force flag: automatically overriding sensitive variables.'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { Config } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { getMockAppFragment } from '../../../__tests__/commands/utils';
|
||||
import { EnvironmentVariableEnvironment } from '../../../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../../../build/utils/environment';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableScope,
|
||||
@@ -12,12 +12,18 @@ import { EnvironmentVariableMutation } from '../../../graphql/mutations/Environm
|
||||
import { AppQuery } from '../../../graphql/queries/AppQuery';
|
||||
import { EnvironmentVariablesQuery } from '../../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import { promptAsync, selectAsync, toggleConfirmAsync } from '../../../prompts';
|
||||
import {
|
||||
parseVisibility,
|
||||
promptVariableEnvironmentAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../../utils/prompts';
|
||||
import EnvUpdate from '../update';
|
||||
|
||||
jest.mock('../../../graphql/queries/EnvironmentVariablesQuery');
|
||||
jest.mock('../../../graphql/mutations/EnvironmentVariableMutation');
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('../../../utils/prompts');
|
||||
jest.mock('../../../graphql/queries/AppQuery');
|
||||
jest.mock('../../../log');
|
||||
|
||||
@@ -34,6 +40,41 @@ describe(EnvUpdate, () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
jest.mocked(AppQuery.byIdAsync).mockImplementation(async () => getMockAppFragment());
|
||||
(jest.mocked(promptVariableEnvironmentAsync) as jest.MockedFunction<any>).mockImplementation(
|
||||
async (args: any) => {
|
||||
if (args.nonInteractive) {
|
||||
throw new Error(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
}
|
||||
if (args.multiple) {
|
||||
return ['production'];
|
||||
}
|
||||
return 'production';
|
||||
}
|
||||
);
|
||||
(jest.mocked(promptVariableVisibilityAsync) as jest.MockedFunction<any>).mockResolvedValue(
|
||||
EnvironmentVariableVisibility.Public
|
||||
);
|
||||
(jest.mocked(parseVisibility) as jest.MockedFunction<any>).mockImplementation(
|
||||
(visibility: string) => {
|
||||
switch (visibility) {
|
||||
case 'plaintext':
|
||||
return EnvironmentVariableVisibility.Public;
|
||||
case 'sensitive':
|
||||
return EnvironmentVariableVisibility.Sensitive;
|
||||
case 'secret':
|
||||
return EnvironmentVariableVisibility.Secret;
|
||||
default:
|
||||
throw new Error(`Invalid visibility: ${visibility}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
(jest.mocked(selectAsync) as jest.MockedFunction<any>).mockResolvedValue({
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
});
|
||||
});
|
||||
|
||||
it('updates a project variable by selected name in non-interactive mode', async () => {
|
||||
@@ -42,7 +83,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -74,7 +115,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
visibility: EnvironmentVariableVisibility.Public,
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
@@ -88,7 +129,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'TEST_VARIABLE',
|
||||
scope: EnvironmentVariableScope.Shared,
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
},
|
||||
];
|
||||
(EnvironmentVariablesQuery.sharedAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
@@ -120,7 +161,7 @@ describe(EnvUpdate, () => {
|
||||
id: variableId,
|
||||
name: 'NEW_VARIABLE',
|
||||
value: 'new-value',
|
||||
environments: [EnvironmentVariableEnvironment.Production],
|
||||
environments: [DefaultEnvironment.Production],
|
||||
});
|
||||
expect(Log.withTick).toHaveBeenCalledWith(
|
||||
`Updated variable ${chalk.bold('TEST_VARIABLE')} on account testuser.`
|
||||
@@ -130,6 +171,7 @@ describe(EnvUpdate, () => {
|
||||
it('prompts for variable selection when selected name is not provided', async () => {
|
||||
const mockVariables = [
|
||||
{ id: variableId, name: 'TEST_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
{ id: '2', name: 'ANOTHER_VARIABLE', scope: EnvironmentVariableScope.Project },
|
||||
];
|
||||
(EnvironmentVariablesQuery.byAppIdAsync as jest.Mock).mockResolvedValue(mockVariables);
|
||||
(EnvironmentVariableMutation.updateAsync as jest.Mock).mockResolvedValue(mockVariables[0]);
|
||||
@@ -141,7 +183,7 @@ describe(EnvUpdate, () => {
|
||||
jest.spyOn(command, 'getContextAsync').mockReturnValue(mockContext);
|
||||
await command.runAsync();
|
||||
|
||||
expect(promptAsync).toHaveBeenCalled();
|
||||
expect(selectAsync).toHaveBeenCalled();
|
||||
expect(EnvironmentVariableMutation.updateAsync).toHaveBeenCalledWith(
|
||||
graphqlClient,
|
||||
expect.objectContaining({ id: variableId })
|
||||
@@ -169,7 +211,7 @@ describe(EnvUpdate, () => {
|
||||
id: 'var1',
|
||||
name: 'TEST_VAR_1',
|
||||
value: 'value1',
|
||||
environments: [EnvironmentVariableEnvironment.Development],
|
||||
environments: [DefaultEnvironment.Development],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
scope: EnvironmentVariableScope.Project,
|
||||
@@ -205,7 +247,7 @@ describe(EnvUpdate, () => {
|
||||
|
||||
expect(EnvironmentVariablesQuery.byAppIdAsync).toHaveBeenCalledWith(graphqlClient, {
|
||||
appId: projectId,
|
||||
environment: EnvironmentVariableEnvironment.Development,
|
||||
environment: DefaultEnvironment.Development,
|
||||
filterNames: ['TEST_VAR_1'],
|
||||
});
|
||||
});
|
||||
|
||||
+22
-24
@@ -3,8 +3,8 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
interface RawCreateFlags {
|
||||
name?: string;
|
||||
@@ -42,7 +41,7 @@ interface RawCreateFlags {
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -53,7 +52,7 @@ interface CreateFlags {
|
||||
type?: 'string' | 'file';
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -64,7 +63,7 @@ export default class EnvCreate extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to create the variable in. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to create the variable in. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -101,6 +100,13 @@ export default class EnvCreate extends EasCommand {
|
||||
|
||||
const validatedFlags = this.sanitizeFlags(flags);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive: validatedFlags['non-interactive'],
|
||||
});
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
@@ -111,14 +117,7 @@ export default class EnvCreate extends EasCommand {
|
||||
force,
|
||||
type,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(validatedFlags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
} = await this.getContextAsync(EnvCreate, {
|
||||
nonInteractive,
|
||||
});
|
||||
} = await this.promptForMissingFlagsAsync(validatedFlags, args, { graphqlClient, projectId });
|
||||
|
||||
const [projectDisplayName, ownerAccount] = await Promise.all([
|
||||
getDisplayNameForProjectIdAsync(graphqlClient, projectId),
|
||||
@@ -272,7 +271,8 @@ export default class EnvCreate extends EasCommand {
|
||||
type,
|
||||
...rest
|
||||
}: CreateFlags,
|
||||
{ environment }: { environment?: string }
|
||||
{ environment }: { environment?: string },
|
||||
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
|
||||
): Promise<
|
||||
Required<
|
||||
Omit<CreateFlags, 'type' | 'visibility'> & {
|
||||
@@ -323,18 +323,16 @@ export default class EnvCreate extends EasCommand {
|
||||
|
||||
value = environmentFilePath ? await fs.readFile(environmentFilePath, 'base64') : value;
|
||||
|
||||
if (environment && !isEnvironment(environment.toLowerCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
let newEnvironments = environments
|
||||
? environments
|
||||
: environment
|
||||
? [environment.toLowerCase() as EnvironmentVariableEnvironment]
|
||||
: undefined;
|
||||
let newEnvironments = environments ? environments : environment ? [environment] : undefined;
|
||||
|
||||
if (!newEnvironments) {
|
||||
newEnvironments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
newEnvironments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (!newEnvironments || newEnvironments.length === 0) {
|
||||
throw new Error('No environments selected');
|
||||
|
||||
+5
-12
@@ -2,7 +2,6 @@ import { Flags } from '@oclif/core';
|
||||
import assert from 'assert';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
@@ -15,18 +14,18 @@ import { EnvironmentVariableMutation } from '../../graphql/mutations/Environment
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptAsync, toggleConfirmAsync } from '../../prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariableName } from '../../utils/variableUtils';
|
||||
|
||||
interface DeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
scope: EnvironmentVariableScope;
|
||||
}
|
||||
|
||||
interface RawDeleteFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
}
|
||||
@@ -38,7 +37,7 @@ export default class EnvDelete extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable to delete',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to delete',
|
||||
}),
|
||||
@@ -50,7 +49,7 @@ export default class EnvDelete extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to delete. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable to delete. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -166,12 +165,6 @@ export default class EnvDelete extends EasCommand {
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toLowerCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return { ...flags, 'variable-environment': environment, scope };
|
||||
}
|
||||
|
||||
|
||||
+9
-13
@@ -1,24 +1,22 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EASNonInteractiveFlag } from '../../commandUtils/flags';
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
type ParsedFlags =
|
||||
| {
|
||||
nonInteractive: true;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
command: string;
|
||||
}
|
||||
| {
|
||||
nonInteractive: false;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
@@ -44,7 +42,7 @@ export default class EnvExec extends EasCommand {
|
||||
name: 'environment',
|
||||
required: true,
|
||||
description:
|
||||
"Environment to execute the command in. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to execute the command in. Default environments are 'production', 'preview', and 'development'.",
|
||||
},
|
||||
{
|
||||
name: 'bash_command',
|
||||
@@ -70,7 +68,11 @@ export default class EnvExec extends EasCommand {
|
||||
|
||||
const environment =
|
||||
parsedFlags.environment ??
|
||||
(await promptVariableEnvironmentAsync({ nonInteractive: parsedFlags.nonInteractive }));
|
||||
(await promptVariableEnvironmentAsync({
|
||||
nonInteractive: parsedFlags.nonInteractive,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
}));
|
||||
const environmentVariables = await this.loadEnvironmentVariablesAsync({
|
||||
graphqlClient,
|
||||
projectId,
|
||||
@@ -101,12 +103,6 @@ export default class EnvExec extends EasCommand {
|
||||
);
|
||||
}
|
||||
|
||||
environment = environment?.toLowerCase();
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const firstChar = bash_command[0];
|
||||
const lastChar = bash_command[bash_command.length - 1];
|
||||
const cleanCommand =
|
||||
@@ -198,7 +194,7 @@ export default class EnvExec extends EasCommand {
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
projectId: string;
|
||||
environment: EnvironmentVariableEnvironment;
|
||||
environment: string;
|
||||
nonInteractive: boolean;
|
||||
}): Promise<Record<string, string>> {
|
||||
const environmentVariablesQueryResult =
|
||||
|
||||
+8
-13
@@ -1,7 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -19,11 +18,11 @@ import {
|
||||
import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVariablesQuery';
|
||||
import Log from '../../log';
|
||||
import { promptVariableEnvironmentAsync, promptVariableNameAsync } from '../../utils/prompts';
|
||||
import { formatVariable, formatVariableValue, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariable, formatVariableValue } from '../../utils/variableUtils';
|
||||
|
||||
interface RawGetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
@@ -31,7 +30,7 @@ interface RawGetFlags {
|
||||
|
||||
interface GetFlags {
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
format?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
@@ -49,7 +48,7 @@ export default class EnvGet extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -58,7 +57,7 @@ export default class EnvGet extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable',
|
||||
}),
|
||||
@@ -93,6 +92,8 @@ export default class EnvGet extends EasCommand {
|
||||
environment = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: false,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -156,12 +157,6 @@ export default class EnvGet extends EasCommand {
|
||||
? EnvironmentVariableScope.Shared
|
||||
: EnvironmentVariableScope.Project;
|
||||
if (environment) {
|
||||
environment = environment.toLowerCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
@@ -177,7 +172,7 @@ async function getVariablesAsync(
|
||||
scope: string,
|
||||
projectId: string,
|
||||
name: string | undefined,
|
||||
environment: EnvironmentVariableEnvironment | undefined
|
||||
environment: string | undefined
|
||||
): Promise<EnvironmentVariableFragment[]> {
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
|
||||
+11
-12
@@ -1,7 +1,6 @@
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
@@ -20,7 +19,6 @@ import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import {
|
||||
formatVariable,
|
||||
formatVariableValue,
|
||||
isEnvironment,
|
||||
performForEnvironmentsAsync,
|
||||
} from '../../utils/variableUtils';
|
||||
|
||||
@@ -36,7 +34,7 @@ async function getVariablesForScopeAsync(
|
||||
scope: EnvironmentVariableScope;
|
||||
includingSensitive: boolean;
|
||||
includeFileContent: boolean;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
projectId: string;
|
||||
}
|
||||
): Promise<EnvironmentVariableWithFileContent[]> {
|
||||
@@ -71,7 +69,7 @@ async function getVariablesForScopeAsync(
|
||||
interface RawListFlags {
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
environment: string[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive'?: boolean;
|
||||
@@ -80,7 +78,7 @@ interface RawListFlags {
|
||||
interface ListFlags {
|
||||
scope: EnvironmentVariableScope;
|
||||
format: string;
|
||||
environment: EnvironmentVariableEnvironment[] | undefined;
|
||||
environment: string[] | undefined;
|
||||
'include-sensitive': boolean;
|
||||
'include-file-content': boolean;
|
||||
'non-interactive': boolean;
|
||||
@@ -112,7 +110,7 @@ export default class EnvList extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to list the variables from. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to list the variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -137,7 +135,12 @@ export default class EnvList extends EasCommand {
|
||||
});
|
||||
|
||||
if (!environments) {
|
||||
environments = await promptVariableEnvironmentAsync({ nonInteractive, multiple: true });
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
await performForEnvironmentsAsync(environments, async environment => {
|
||||
@@ -180,14 +183,10 @@ export default class EnvList extends EasCommand {
|
||||
flags: RawListFlags,
|
||||
{ environment }: { environment?: string }
|
||||
): ListFlags {
|
||||
if (environment && !isEnvironment(environment.toLowerCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments = flags.environment
|
||||
? flags.environment
|
||||
: environment
|
||||
? [environment.toLowerCase() as EnvironmentVariableEnvironment]
|
||||
? [environment]
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
+9
-10
@@ -13,7 +13,6 @@ import {
|
||||
import Log from '../../log';
|
||||
import { confirmAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvPull extends EasCommand {
|
||||
static override description =
|
||||
@@ -29,7 +28,7 @@ export default class EnvPull extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to pull variables from. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to pull variables from. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -51,14 +50,6 @@ export default class EnvPull extends EasCommand {
|
||||
|
||||
let environment = flagEnvironment?.toLowerCase() ?? argEnvironment?.toLowerCase();
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync({ nonInteractive });
|
||||
}
|
||||
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const {
|
||||
projectId,
|
||||
loggedIn: { graphqlClient },
|
||||
@@ -67,6 +58,14 @@ export default class EnvPull extends EasCommand {
|
||||
nonInteractive,
|
||||
});
|
||||
|
||||
if (!environment) {
|
||||
environment = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
targetPath = targetPath ?? '.env.local';
|
||||
|
||||
const environmentVariables = await EnvironmentVariablesQuery.byAppIdWithSensitiveAsync(
|
||||
|
||||
+56
-50
@@ -3,7 +3,6 @@ import dotenv from 'dotenv';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { EASMultiEnvironmentFlag } from '../../commandUtils/flags';
|
||||
import {
|
||||
@@ -17,7 +16,6 @@ import { EnvironmentVariablesQuery } from '../../graphql/queries/EnvironmentVari
|
||||
import Log from '../../log';
|
||||
import { confirmAsync, promptAsync } from '../../prompts';
|
||||
import { promptVariableEnvironmentAsync } from '../../utils/prompts';
|
||||
import { isEnvironment } from '../../utils/variableUtils';
|
||||
|
||||
export default class EnvPush extends EasCommand {
|
||||
static override description =
|
||||
@@ -34,13 +32,17 @@ export default class EnvPush extends EasCommand {
|
||||
description: 'Path to the input `.env` file',
|
||||
default: '.env.local',
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
description: 'Skip confirmation and automatically override existing variables',
|
||||
default: false,
|
||||
}),
|
||||
};
|
||||
|
||||
static override args = [
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Environment to push variables to. One of 'production', 'preview', or 'development'.",
|
||||
"Environment to push variables to. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -48,7 +50,7 @@ export default class EnvPush extends EasCommand {
|
||||
async runAsync(): Promise<void> {
|
||||
const { args, flags } = await this.parse(EnvPush);
|
||||
|
||||
let { environment: environments, path: envPath } = this.parseFlagsAndArgs(flags, args);
|
||||
let { environment: environments, path: envPath, force } = this.parseFlagsAndArgs(flags, args);
|
||||
|
||||
const {
|
||||
projectId,
|
||||
@@ -61,6 +63,9 @@ export default class EnvPush extends EasCommand {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive: false,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -114,40 +119,46 @@ export default class EnvPush extends EasCommand {
|
||||
Log.warn(`Some variables already exist in the ${displayedEnvironment} environment.`);
|
||||
const variableNames = existingDifferentVariables.map(variable => variable.name);
|
||||
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
|
||||
let variablesToOverwrite: string[] = [];
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 0) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
if (force) {
|
||||
// When --force is used, automatically override all existing variables
|
||||
Log.log('Using --force flag: automatically overriding existing variables.');
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
const confirmationMessage =
|
||||
variableNames.length > 1
|
||||
? `The ${variableNames.join(
|
||||
', '
|
||||
)} environment variables already exist in ${displayedEnvironment} environment. Do you want to override them all?`
|
||||
: `The ${variableNames[0]} environment variable already exists in ${displayedEnvironment} environment. Do you want to override it?`;
|
||||
|
||||
const confirm = await confirmAsync({
|
||||
message: confirmationMessage,
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
|
||||
if (!confirm && existingDifferentVariables.length === 0) {
|
||||
throw new Error('No new variables to push.');
|
||||
}
|
||||
|
||||
if (confirm) {
|
||||
variablesToOverwrite = existingDifferentVariables.map(variable => variable.name);
|
||||
} else {
|
||||
const promptResult = await promptAsync({
|
||||
type: 'multiselect',
|
||||
name: 'variablesToOverwrite',
|
||||
message: 'Select variables to overwrite:',
|
||||
// @ts-expect-error property missing from `@types/prompts`
|
||||
optionsPerPage: 20,
|
||||
choices: existingDifferentVariables.map(variable => ({
|
||||
title: `${variable.name}: ${updateVariables[variable.name].value} (was ${
|
||||
variable.value ?? '(secret)'
|
||||
})`,
|
||||
value: variable.name,
|
||||
})),
|
||||
});
|
||||
variablesToOverwrite = promptResult.variablesToOverwrite;
|
||||
}
|
||||
}
|
||||
|
||||
for (const existingVariable of existingVariables) {
|
||||
@@ -165,7 +176,7 @@ export default class EnvPush extends EasCommand {
|
||||
variable => variable.visibility !== EnvironmentVariableVisibility.Public
|
||||
);
|
||||
|
||||
if (existingSensitiveVariables.length > 0) {
|
||||
if (existingSensitiveVariables.length > 0 && !force) {
|
||||
const existingSensitiveVariablesNames = existingSensitiveVariables.map(
|
||||
variable => `- ${variable.name}`
|
||||
);
|
||||
@@ -177,6 +188,8 @@ export default class EnvPush extends EasCommand {
|
||||
if (!confirm) {
|
||||
throw new Error('Aborting...');
|
||||
}
|
||||
} else if (existingSensitiveVariables.length > 0 && force) {
|
||||
Log.log('Using --force flag: automatically overriding sensitive variables.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,16 +209,14 @@ export default class EnvPush extends EasCommand {
|
||||
}
|
||||
|
||||
parseFlagsAndArgs(
|
||||
flags: { path: string; environment: EnvironmentVariableEnvironment[] | undefined },
|
||||
flags: {
|
||||
path: string;
|
||||
environment: string[] | undefined;
|
||||
force: boolean;
|
||||
},
|
||||
{ environment }: Record<string, string>
|
||||
): { environment?: EnvironmentVariableEnvironment[]; path: string } {
|
||||
if (environment && !isEnvironment(environment.toLowerCase())) {
|
||||
throw new Error("Invalid environment. Use one of 'production', 'preview', or 'development'.");
|
||||
}
|
||||
|
||||
const environments =
|
||||
flags.environment ??
|
||||
(environment ? [environment.toLowerCase() as EnvironmentVariableEnvironment] : undefined);
|
||||
): { environment?: string[]; path: string; force: boolean } {
|
||||
const environments = flags.environment ?? (environment ? [environment] : undefined);
|
||||
|
||||
return {
|
||||
...flags,
|
||||
@@ -215,13 +226,8 @@ export default class EnvPush extends EasCommand {
|
||||
|
||||
private async parseEnvFileAsync(
|
||||
envPath: string,
|
||||
environments: EnvironmentVariableEnvironment[]
|
||||
): Promise<
|
||||
Record<
|
||||
string,
|
||||
CreateEnvironmentVariableInput & { environments: EnvironmentVariableEnvironment[] }
|
||||
>
|
||||
> {
|
||||
environments: string[]
|
||||
): Promise<Record<string, CreateEnvironmentVariableInput & { environments: string[] }>> {
|
||||
if (!(await fs.exists(envPath))) {
|
||||
throw new Error(`File ${envPath} does not exist.`);
|
||||
}
|
||||
|
||||
+26
-24
@@ -4,8 +4,8 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import {
|
||||
EASEnvironmentVariableScopeFlag,
|
||||
EASEnvironmentVariableScopeFlagValue,
|
||||
@@ -36,17 +36,17 @@ import {
|
||||
promptVariableValueAsync,
|
||||
promptVariableVisibilityAsync,
|
||||
} from '../../utils/prompts';
|
||||
import { formatVariableName, isEnvironment } from '../../utils/variableUtils';
|
||||
import { formatVariableName } from '../../utils/variableUtils';
|
||||
|
||||
interface RawUpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope: EASEnvironmentVariableScopeFlagValue;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -54,11 +54,11 @@ interface UpdateFlags {
|
||||
name?: string;
|
||||
value?: string;
|
||||
scope: EnvironmentVariableScope;
|
||||
environment?: EnvironmentVariableEnvironment[];
|
||||
environment?: string[];
|
||||
visibility?: 'plaintext' | 'sensitive' | 'secret';
|
||||
type?: 'string' | 'file';
|
||||
'variable-name'?: string;
|
||||
'variable-environment'?: EnvironmentVariableEnvironment;
|
||||
'variable-environment'?: string;
|
||||
'non-interactive': boolean;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ export default class EnvUpdate extends EasCommand {
|
||||
'variable-name': Flags.string({
|
||||
description: 'Current name of the variable',
|
||||
}),
|
||||
'variable-environment': Flags.enum<EnvironmentVariableEnvironment>({
|
||||
'variable-environment': Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description: 'Current environment of the variable to update',
|
||||
}),
|
||||
@@ -93,7 +93,7 @@ export default class EnvUpdate extends EasCommand {
|
||||
{
|
||||
name: 'environment',
|
||||
description:
|
||||
"Current environment of the variable to update. One of 'production', 'preview', or 'development'.",
|
||||
"Current environment of the variable to update. Default environments are 'production', 'preview', and 'development'.",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
@@ -180,15 +180,19 @@ export default class EnvUpdate extends EasCommand {
|
||||
visibility: newVisibility,
|
||||
type: newType,
|
||||
fileName,
|
||||
} = await this.promptForMissingFlagsAsync(selectedVariable, {
|
||||
name,
|
||||
value: rawValue,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
});
|
||||
} = await this.promptForMissingFlagsAsync(
|
||||
selectedVariable,
|
||||
{
|
||||
name,
|
||||
value: rawValue,
|
||||
environment: environments,
|
||||
visibility,
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
scope,
|
||||
},
|
||||
{ graphqlClient, projectId }
|
||||
);
|
||||
|
||||
const variable = await EnvironmentVariableMutation.updateAsync(graphqlClient, {
|
||||
id: selectedVariable.id,
|
||||
@@ -226,12 +230,6 @@ export default class EnvUpdate extends EasCommand {
|
||||
: EnvironmentVariableScope.Project;
|
||||
|
||||
if (environment) {
|
||||
environment = environment.toLowerCase();
|
||||
if (!isEnvironment(environment)) {
|
||||
throw new Error(
|
||||
"Invalid environment. Use one of 'production', 'preview', or 'development'."
|
||||
);
|
||||
}
|
||||
return {
|
||||
...flags,
|
||||
'variable-environment': environment,
|
||||
@@ -252,7 +250,8 @@ export default class EnvUpdate extends EasCommand {
|
||||
'non-interactive': nonInteractive,
|
||||
type,
|
||||
...rest
|
||||
}: UpdateFlags
|
||||
}: UpdateFlags,
|
||||
{ graphqlClient, projectId }: { graphqlClient: ExpoGraphqlClient; projectId: string }
|
||||
): Promise<
|
||||
Omit<UpdateFlags, 'type' | 'visibility'> & {
|
||||
type?: EnvironmentSecretType;
|
||||
@@ -316,7 +315,10 @@ export default class EnvUpdate extends EasCommand {
|
||||
environments = await promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
multiple: true,
|
||||
canEnterCustomEnvironment: true,
|
||||
selectedEnvironments: selectedVariable.environments ?? [],
|
||||
graphqlClient,
|
||||
projectId,
|
||||
});
|
||||
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,6 @@ import chalk from 'chalk';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { selectBranchOnAppAsync } from '../../branch/queries';
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { fetchBuildsAsync, formatBuild } from '../../commandUtils/builds';
|
||||
import { GetServerSideEnvironmentVariablesFn } from '../../commandUtils/context/ServerSideEnvironmentVariablesContextField';
|
||||
@@ -97,7 +96,7 @@ export default class FingerprintCompare extends EasCommand {
|
||||
open: Flags.boolean({
|
||||
description: 'Open the fingerprint comparison in the browser',
|
||||
}),
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
description:
|
||||
'If generating a fingerprint from the local directory, use the specified environment.',
|
||||
|
||||
@@ -4,7 +4,6 @@ import { Flags } from '@oclif/core';
|
||||
|
||||
import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { evaluateConfigWithEnvVarsAsync } from '../../build/evaluateConfigWithEnvVarsAsync';
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import {
|
||||
EASEnvironmentFlag,
|
||||
@@ -40,7 +39,7 @@ export default class FingerprintGenerate extends EasCommand {
|
||||
options: ['android', 'ios'],
|
||||
}),
|
||||
...EASEnvironmentFlag,
|
||||
environment: Flags.enum<EnvironmentVariableEnvironment>({
|
||||
environment: Flags.string({
|
||||
...EasEnvironmentFlagParameters,
|
||||
exclusive: ['build-profile'],
|
||||
}),
|
||||
|
||||
@@ -1,49 +1,29 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import fs from 'fs-extra';
|
||||
import merge from 'ts-deepmerge';
|
||||
|
||||
import { getEASUpdateURL } from '../../../api';
|
||||
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { jester, jester2 } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppFragment, Role } from '../../../graphql/generated';
|
||||
import { jester } from '../../../credentials/__tests__/fixtures-constants';
|
||||
import { AppMutation } from '../../../graphql/mutations/AppMutation';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../../onboarding/git';
|
||||
import {
|
||||
installDependenciesAsync,
|
||||
promptForPackageManagerAsync,
|
||||
} from '../../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../../onboarding/runCommand';
|
||||
import { Ora, ora } from '../../../ora';
|
||||
import {
|
||||
createOrModifyExpoConfigAsync,
|
||||
getPrivateExpoConfigAsync,
|
||||
} from '../../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { promptAsync } from '../../../prompts';
|
||||
import { Actor, getActorUsername } from '../../../user/User';
|
||||
import New, {
|
||||
cloneTemplateAsync,
|
||||
copyProjectTemplatesAsync,
|
||||
createProjectAsync,
|
||||
formatScriptCommand,
|
||||
generateAppConfigAsync,
|
||||
generateEasConfigAsync,
|
||||
getAccountChoices,
|
||||
initializeGitRepositoryAsync,
|
||||
installProjectDependenciesAsync,
|
||||
mergeReadmeAsync,
|
||||
promptForTargetDirectoryAsync,
|
||||
updatePackageJsonAsync,
|
||||
} from '../new';
|
||||
import New, { createProjectAsync, generateConfigsAsync } from '../new';
|
||||
|
||||
jest.mock('../../../prompts');
|
||||
jest.mock('fs-extra', () => ({
|
||||
...jest.requireActual('fs-extra'),
|
||||
pathExists: jest.fn().mockResolvedValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../../../prompts', () => ({
|
||||
promptAsync: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../onboarding/git');
|
||||
jest.mock('../../../onboarding/runCommand');
|
||||
jest.mock('../../../graphql/mutations/AppMutation');
|
||||
jest.mock('../../../project/expoConfig');
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync');
|
||||
jest.mock('../../../user/User', () => ({
|
||||
getActorUsername: jest.fn(),
|
||||
getActorUsername: jest.fn().mockReturnValue('jester'),
|
||||
}));
|
||||
jest.mock('../../../ora');
|
||||
jest.mock('fs-extra');
|
||||
@@ -53,662 +33,81 @@ jest.mock('../../../build/utils/url');
|
||||
jest.mock('../../../utils/easCli', () => ({
|
||||
easCliVersion: '5.0.0',
|
||||
}));
|
||||
// Remove Log module mocking entirely - use the actual module
|
||||
|
||||
jest.mock('../../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync', () => ({
|
||||
findProjectIdByAccountNameAndSlugNullableAsync: jest.fn().mockResolvedValue(null),
|
||||
}));
|
||||
|
||||
jest.mock('../../../graphql/mutations/AppMutation', () => ({
|
||||
AppMutation: {
|
||||
createAppAsync: jest.fn().mockResolvedValue('test-project-id'),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../../project/expoConfig', () => ({
|
||||
getPrivateExpoConfigAsync: jest.fn().mockResolvedValue({ extra: {} }),
|
||||
createOrModifyExpoConfigAsync: jest.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
jest.mock('../../../prompts', () => ({
|
||||
...jest.requireActual('../../../prompts'),
|
||||
promptAsync: jest.fn().mockResolvedValue({ account: { name: 'jester' } }),
|
||||
}));
|
||||
|
||||
describe(New.name, () => {
|
||||
let consoleLogSpy: jest.SpyInstance;
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
describe('generateConfigsAsync', () => {
|
||||
it('should orchestrate account selection and project configuration', async () => {
|
||||
const result = await generateConfigsAsync({ path: undefined }, jester, mockGraphqlClient);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
expect(result).toEqual({
|
||||
projectAccount: 'jester',
|
||||
projectDirectory: expect.stringContaining('/new-expo-project'),
|
||||
projectName: 'new-expo-project',
|
||||
});
|
||||
});
|
||||
|
||||
// Capture console.log calls
|
||||
consoleLogSpy = jest.spyOn(console, 'log');
|
||||
it('should pass through path argument to config generation', async () => {
|
||||
const customPath = 'my-custom-project';
|
||||
|
||||
jest.mocked(fs.writeFile).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readJson).mockImplementation(() => Promise.resolve({}));
|
||||
jest.mocked(fs.writeJson).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.copy).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.remove).mockImplementation(() => Promise.resolve());
|
||||
jest.mocked(fs.readFile).mockImplementation(() => Promise.resolve(''));
|
||||
const result = await generateConfigsAsync({ path: customPath }, jester, mockGraphqlClient);
|
||||
|
||||
expect(result).toEqual({
|
||||
projectAccount: 'jester',
|
||||
projectDirectory: expect.stringContaining('/my-custom-project'),
|
||||
projectName: 'my-custom-project',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
});
|
||||
describe('createProjectAsync', () => {
|
||||
it('should create a project', async () => {
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue('test-project-id');
|
||||
jest
|
||||
.mocked(getPrivateExpoConfigAsync)
|
||||
.mockResolvedValue({ name: 'name', slug: 'slug' } as ExpoConfig);
|
||||
jest.mocked(createOrModifyExpoConfigAsync);
|
||||
|
||||
// Helper function to get all console output as strings
|
||||
const getConsoleOutput = (): string[] => {
|
||||
return consoleLogSpy.mock.calls.map(call => (call.length === 0 ? '' : call.join(' ')));
|
||||
};
|
||||
|
||||
// Helper function to check if a specific message was logged
|
||||
const expectConsoleToContain = (message: string): void => {
|
||||
const output = getConsoleOutput();
|
||||
// strip out ANSI codes and special characters like the tick
|
||||
const outputWithoutAnsi = output.map(line =>
|
||||
line.replace(/\x1b\[[0-9;]*m/g, '').replace(/✔\s*/, '')
|
||||
);
|
||||
// throw new Error(message + '\n' + outputWithoutAnsi.join('\n'));
|
||||
expect(outputWithoutAnsi.some(line => line.includes(message))).toBeTruthy();
|
||||
};
|
||||
|
||||
// Helper function to verify prompt was called with expected message
|
||||
const expectPromptToHaveMessage = (expectedMessage: string): void => {
|
||||
expect(promptAsync).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
message: expectedMessage,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to simulate user input via prompt
|
||||
const mockUserInput = (inputValue: any): void => {
|
||||
jest.mocked(promptAsync).mockResolvedValue(inputValue);
|
||||
};
|
||||
|
||||
describe('helper functions', () => {
|
||||
describe('promptForTargetDirectoryAsync', () => {
|
||||
it('should prompt for target directory', async () => {
|
||||
const promptedDirectory = '/test/prompted-project';
|
||||
mockUserInput({ targetProjectDir: promptedDirectory });
|
||||
|
||||
const result = await promptForTargetDirectoryAsync();
|
||||
|
||||
expectConsoleToContain(
|
||||
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
|
||||
);
|
||||
expectPromptToHaveMessage('Where would you like to create your new project directory?');
|
||||
expect(result).toBe(promptedDirectory);
|
||||
const result = await createProjectAsync({
|
||||
graphqlClient: mockGraphqlClient,
|
||||
actor: jester,
|
||||
projectDirectory: 'test-project',
|
||||
projectAccount: 'jester',
|
||||
projectName: 'test',
|
||||
});
|
||||
|
||||
it('should handle the target directory from args', async () => {
|
||||
const providedDirectory = '/test/provided-project';
|
||||
const result = await promptForTargetDirectoryAsync(providedDirectory);
|
||||
|
||||
expectConsoleToContain(
|
||||
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
|
||||
);
|
||||
|
||||
expect(promptAsync).not.toHaveBeenCalled();
|
||||
|
||||
expect(result).toBe(providedDirectory);
|
||||
expect(result).toBe('test-project-id');
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
|
||||
accountId: jester.accounts[0].id,
|
||||
projectName: 'test',
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneTemplateAsync', () => {
|
||||
it('should clone the template project with ssh', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(true);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
expectConsoleToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
expectConsoleToContain('We detected that ssh is your preferred git clone method');
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'ssh',
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
|
||||
it('should clone the template project with https', async () => {
|
||||
const targetProjectDir = '/test/target-project';
|
||||
const finalTargetProjectDir = '/test/final-target-project';
|
||||
|
||||
jest.mocked(canAccessRepositoryUsingSshAsync).mockResolvedValue(false);
|
||||
jest.mocked(runGitCloneAsync).mockResolvedValue({
|
||||
targetProjectDir: finalTargetProjectDir,
|
||||
});
|
||||
|
||||
const result = await cloneTemplateAsync(targetProjectDir);
|
||||
|
||||
expectConsoleToContain(`📂 Cloning the project to ${targetProjectDir}`);
|
||||
expectConsoleToContain('We detected that https is your preferred git clone method');
|
||||
|
||||
expect(runGitCloneAsync).toHaveBeenCalledWith({
|
||||
githubUsername: 'expo',
|
||||
githubRepositoryName: 'expo-template-default',
|
||||
targetProjectDir,
|
||||
cloneMethod: 'https',
|
||||
});
|
||||
|
||||
expect(result).toBe(finalTargetProjectDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('installProjectDependenciesAsync', () => {
|
||||
it('should install the project dependencies', async () => {
|
||||
const projectDir = '/test/project-directory';
|
||||
|
||||
jest.mocked(promptForPackageManagerAsync).mockResolvedValue('npm');
|
||||
jest.mocked(installDependenciesAsync).mockResolvedValue();
|
||||
jest.mocked(runCommandAsync).mockResolvedValue();
|
||||
|
||||
const packageManager = await installProjectDependenciesAsync(projectDir);
|
||||
expect(packageManager).toBe('npm');
|
||||
|
||||
expect(promptForPackageManagerAsync).toHaveBeenCalled();
|
||||
expect(installDependenciesAsync).toHaveBeenCalledWith({
|
||||
projectDir,
|
||||
packageManager: 'npm',
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', 'expo-updates'],
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', '@expo/metro-runtime'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAccountChoices', () => {
|
||||
it('should return mapped account choices', () => {
|
||||
// Use jester fixture which has multiple accounts with different permissions
|
||||
const namesWithSufficientPermissions = new Set(['jester']); // Only personal account has permissions
|
||||
|
||||
const result = getAccountChoices(jester, namesWithSufficientPermissions);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
title: 'jester (personal account)',
|
||||
value: { name: 'jester' },
|
||||
},
|
||||
{
|
||||
title: 'other',
|
||||
value: { name: 'other' },
|
||||
disabled: true,
|
||||
description:
|
||||
'You do not have the required permissions to create projects on this account.',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createProjectAsync', () => {
|
||||
const mockGraphqlClient = {} as ExpoGraphqlClient;
|
||||
const projectDir = '/test/project-dir';
|
||||
const mockProjectId = 'test-project-id';
|
||||
|
||||
beforeEach(() => {
|
||||
// use single-account jester for most of the tests
|
||||
jest.mocked(getActorUsername).mockReturnValue('jester2');
|
||||
|
||||
jest.mocked(getPrivateExpoConfigAsync).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
extra: {},
|
||||
} as ExpoConfig);
|
||||
|
||||
jest.mocked(createOrModifyExpoConfigAsync).mockResolvedValue({
|
||||
type: 'success' as const,
|
||||
message: 'Config updated',
|
||||
config: {
|
||||
name: 'test-app',
|
||||
slug: 'test-app',
|
||||
extra: { eas: { projectId: 'test-project-id' } },
|
||||
} as ExpoConfig,
|
||||
});
|
||||
|
||||
const mockSpinner: Partial<Ora> = {
|
||||
start: jest.fn().mockReturnThis(),
|
||||
succeed: jest.fn().mockReturnThis(),
|
||||
fail: jest.fn().mockReturnThis(),
|
||||
};
|
||||
jest.mocked(ora).mockReturnValue(mockSpinner as Ora);
|
||||
});
|
||||
|
||||
it('should create a project', async () => {
|
||||
const mockActor = jester2 as Actor;
|
||||
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue(mockProjectId);
|
||||
|
||||
const result = await createProjectAsync(mockGraphqlClient, mockActor, projectDir);
|
||||
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester2',
|
||||
'jester2-app'
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
|
||||
accountId: 'jester2-account-id',
|
||||
projectName: 'jester2-app',
|
||||
});
|
||||
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
projectDir,
|
||||
{
|
||||
extra: { eas: { projectId: mockProjectId } },
|
||||
},
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
|
||||
expect(result).toBe(mockProjectId);
|
||||
});
|
||||
|
||||
it('should create a project when there are multiple accounts', async () => {
|
||||
// Use jester fixture which has multiple accounts
|
||||
const mockActor = jester;
|
||||
|
||||
jest.mocked(getActorUsername).mockReturnValue('jester');
|
||||
jest.mocked(promptAsync).mockResolvedValue({
|
||||
account: { name: 'jester' },
|
||||
});
|
||||
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
jest.mocked(AppMutation.createAppAsync).mockResolvedValue(mockProjectId);
|
||||
|
||||
const result = await createProjectAsync(mockGraphqlClient, mockActor, projectDir);
|
||||
|
||||
expect(promptAsync).toHaveBeenCalledWith({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices: expect.any(Array),
|
||||
});
|
||||
|
||||
expect(findProjectIdByAccountNameAndSlugNullableAsync).toHaveBeenCalledWith(
|
||||
mockGraphqlClient,
|
||||
'jester',
|
||||
'jester-app'
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).toHaveBeenCalledWith(mockGraphqlClient, {
|
||||
accountId: 'jester-account-id',
|
||||
projectName: 'jester-app',
|
||||
});
|
||||
|
||||
expect(result).toBe(mockProjectId);
|
||||
});
|
||||
|
||||
it('should throw when the project already exists', async () => {
|
||||
const mockActor = jester2 as Actor;
|
||||
|
||||
const existingProjectId = 'existing-project-id';
|
||||
jest
|
||||
.mocked(findProjectIdByAccountNameAndSlugNullableAsync)
|
||||
.mockResolvedValue(existingProjectId);
|
||||
|
||||
await expect(createProjectAsync(mockGraphqlClient, mockActor, projectDir)).rejects.toThrow(
|
||||
`Existing project found: @jester2/jester2-app (ID: ${existingProjectId}). Project ID configuration canceled. Re-run the command to select a different account/project.`
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw when the user does not have permission to create projects on account', async () => {
|
||||
// Create a custom actor with ViewOnly permissions for this test
|
||||
const mockActor = merge(jester2, {
|
||||
accounts: [{ users: [{ actor: { id: 'view-only-jester' }, role: Role.ViewOnly }] }],
|
||||
}) as Actor;
|
||||
|
||||
jest.mocked(getActorUsername).mockReturnValue('view-only-jester');
|
||||
jest.mocked(findProjectIdByAccountNameAndSlugNullableAsync).mockResolvedValue(null);
|
||||
|
||||
await expect(createProjectAsync(mockGraphqlClient, mockActor, projectDir)).rejects.toThrow(
|
||||
`You don't have permission to create a new project on the jester account and no matching project already exists on the account.`
|
||||
);
|
||||
|
||||
expect(AppMutation.createAppAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAppConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
const mockUpdateUrl = 'https://u.expo.dev/test-project-id';
|
||||
|
||||
const createMockApp = (overrides: Partial<AppFragment> = {}): AppFragment => ({
|
||||
id: 'test-project-id',
|
||||
name: 'Test App',
|
||||
fullName: '@testowner/test-app',
|
||||
slug: 'test-app',
|
||||
ownerAccount: {
|
||||
id: 'testowner-account-id',
|
||||
name: 'testowner',
|
||||
users: [],
|
||||
expect(createOrModifyExpoConfigAsync).toHaveBeenCalledWith(
|
||||
'test-project',
|
||||
{
|
||||
extra: { eas: { projectId: 'test-project-id' } },
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(getEASUpdateURL).mockReturnValue(mockUpdateUrl);
|
||||
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {} as ExpoConfig,
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate the app config', async () => {
|
||||
const mockApp = createMockApp();
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
expo: {
|
||||
name: 'value-to-override',
|
||||
slug: 'value-to-override',
|
||||
icon: 'value-to-keep',
|
||||
} as ExpoConfig,
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(getEASUpdateURL).toHaveBeenCalledWith('test-project-id', null);
|
||||
|
||||
const expectedConfig = {
|
||||
expo: {
|
||||
name: 'Test App',
|
||||
slug: 'test-app',
|
||||
scheme: 'TestApp',
|
||||
icon: 'value-to-keep',
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: 'test-project-id',
|
||||
},
|
||||
},
|
||||
owner: 'testowner',
|
||||
updates: {
|
||||
url: mockUpdateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier: 'com.testowner.testapp',
|
||||
},
|
||||
android: {
|
||||
package: 'com.testowner.testapp',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/app.json`, expectedConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
expectConsoleToContain('Generated app.json');
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the bundle identifier', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test-app-with-dashes',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-account-id',
|
||||
name: 'test-owner',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithdashes',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the owner account name', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'testapp',
|
||||
ownerAccount: {
|
||||
id: 'test-owner-123-account-id',
|
||||
name: 'test-owner-123',
|
||||
users: [],
|
||||
},
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner123.testapp',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid characters in the slug', async () => {
|
||||
const mockApp = createMockApp({
|
||||
slug: 'test@app#with$symbols',
|
||||
fullName: '@testowner/test@app#with$symbols',
|
||||
});
|
||||
|
||||
await generateAppConfigAsync(projectDir, mockApp);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/app.json`,
|
||||
expect.objectContaining({
|
||||
expo: expect.objectContaining({
|
||||
ios: expect.objectContaining({
|
||||
bundleIdentifier: 'com.testowner.testappwithsymbols',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ spaces: 2 }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateEasConfigAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
it('should generate the eas config', async () => {
|
||||
const expectedEasConfig = {
|
||||
cli: {
|
||||
version: '>= 5.0.0',
|
||||
appVersionSource: 'remote',
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: { simulator: true },
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
android: { image: 'latest' },
|
||||
ios: { image: 'latest' },
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
await generateEasConfigAsync(projectDir);
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(`${projectDir}/eas.json`, expectedEasConfig, {
|
||||
spaces: 2,
|
||||
});
|
||||
|
||||
expectConsoleToContain('Generated eas.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updatePackageJsonAsync', () => {
|
||||
it('should update the package.json', async () => {
|
||||
jest.mocked(fs.readJson).mockResolvedValue({
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
});
|
||||
const projectDir = '/test/project-dir';
|
||||
await updatePackageJsonAsync(projectDir);
|
||||
|
||||
const expectedPackageJson = {
|
||||
name: 'test-app',
|
||||
version: '1.0.0',
|
||||
scripts: {
|
||||
preview: 'npx eas-cli@latest workflow:run publish-preview-update.yml',
|
||||
'development-builds': 'npx eas-cli@latest workflow:run create-development-builds.yml',
|
||||
deploy: 'npx eas-cli@latest workflow:run deploy-to-production.yml',
|
||||
},
|
||||
};
|
||||
|
||||
expect(fs.writeJson).toHaveBeenCalledWith(
|
||||
`${projectDir}/package.json`,
|
||||
expectedPackageJson,
|
||||
{ spaces: 2 }
|
||||
);
|
||||
|
||||
expectConsoleToContain('Updated package.json with scripts');
|
||||
});
|
||||
});
|
||||
|
||||
describe('copyProjectTemplatesAsync', () => {
|
||||
it('should copy the project templates', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
expect(fs.copy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('templates/.eas/workflows'),
|
||||
expect.stringContaining('.eas/workflows'),
|
||||
{ errorOnExist: false, overwrite: true }
|
||||
);
|
||||
|
||||
expectConsoleToContain('Created EAS workflow files');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeReadmeAsync', () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
it('should merge readme when target section is found', async () => {
|
||||
const existingReadme = `# My App
|
||||
|
||||
This is my app.
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
Some content here.`;
|
||||
|
||||
const readmeAdditions = `## Workflows
|
||||
|
||||
This project uses EAS Workflows.`;
|
||||
|
||||
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(readmeAdditions);
|
||||
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(existingReadme);
|
||||
|
||||
await mergeReadmeAsync(projectDir);
|
||||
|
||||
const expectedMergedReadme = `# My App
|
||||
|
||||
This is my app.
|
||||
|
||||
## Workflows
|
||||
|
||||
This project uses EAS Workflows.
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
Some content here.`;
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(`${projectDir}/README.md`, expectedMergedReadme);
|
||||
|
||||
expectConsoleToContain('Updated README.md with EAS configuration details');
|
||||
});
|
||||
|
||||
it('should append readme when target section is not found', async () => {
|
||||
const existingReadme = `# My App
|
||||
|
||||
This is my app.`;
|
||||
|
||||
const readmeAdditions = `## Workflows
|
||||
|
||||
This project uses EAS Workflows.`;
|
||||
|
||||
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(readmeAdditions);
|
||||
(jest.mocked(fs.readFile) as jest.Mock).mockResolvedValueOnce(existingReadme);
|
||||
|
||||
await mergeReadmeAsync(projectDir);
|
||||
|
||||
const expectedMergedReadme = `# My App
|
||||
|
||||
This is my app.
|
||||
|
||||
## Workflows
|
||||
|
||||
This project uses EAS Workflows.
|
||||
`;
|
||||
|
||||
expect(fs.writeFile).toHaveBeenCalledWith(`${projectDir}/README.md`, expectedMergedReadme);
|
||||
|
||||
expectConsoleToContain('Updated README.md with EAS configuration details');
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeGitRepositoryAsync', () => {
|
||||
it('should initialize git repository', async () => {
|
||||
const projectDir = '/test/project-dir';
|
||||
|
||||
await initializeGitRepositoryAsync(projectDir);
|
||||
|
||||
expect(fs.remove).toHaveBeenCalledWith(`${projectDir}/.git`);
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['init'],
|
||||
cwd: projectDir,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['add', '.'],
|
||||
cwd: projectDir,
|
||||
});
|
||||
|
||||
expect(runCommandAsync).toHaveBeenCalledWith({
|
||||
command: 'git',
|
||||
args: ['commit', '-m', 'Initial commit'],
|
||||
cwd: projectDir,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatScriptCommand', () => {
|
||||
it('should format script commands for package managers', () => {
|
||||
expect(formatScriptCommand('start', 'npm')).toBe('npm run start');
|
||||
expect(formatScriptCommand('start', 'yarn')).toBe('yarn start');
|
||||
expect(formatScriptCommand('start', 'pnpm')).toBe('pnpm start');
|
||||
});
|
||||
{ skipSDKVersionRequirement: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,178 +1,75 @@
|
||||
import { ExpoConfig } from '@expo/config';
|
||||
import { AppVersionSource, EasJson } from '@expo/eas-json';
|
||||
import { Flags } from '@oclif/core';
|
||||
import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import nullthrows from 'nullthrows';
|
||||
import path from 'path';
|
||||
import merge from 'ts-deepmerge';
|
||||
|
||||
import { getEASUpdateURL } from '../../api';
|
||||
import { getProjectDashboardUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { AppFragment, Role } from '../../graphql/generated';
|
||||
import {
|
||||
cloneTemplateAsync,
|
||||
initializeGitRepositoryAsync,
|
||||
installProjectDependenciesAsync,
|
||||
} from '../../commandUtils/new/commands';
|
||||
import {
|
||||
generateProjectConfigAsync,
|
||||
promptForProjectAccountAsync,
|
||||
} from '../../commandUtils/new/configs';
|
||||
import {
|
||||
copyProjectTemplatesAsync,
|
||||
generateAppConfigAsync,
|
||||
generateEasConfigAsync,
|
||||
updatePackageJsonAsync,
|
||||
updateReadmeAsync,
|
||||
} from '../../commandUtils/new/projectFiles';
|
||||
import { AppFragment } from '../../graphql/generated';
|
||||
import { AppMutation } from '../../graphql/mutations/AppMutation';
|
||||
import { AppQuery } from '../../graphql/queries/AppQuery';
|
||||
import Log, { learnMore, link } from '../../log';
|
||||
import { canAccessRepositoryUsingSshAsync, runGitCloneAsync } from '../../onboarding/git';
|
||||
import {
|
||||
PackageManager,
|
||||
installDependenciesAsync,
|
||||
promptForPackageManagerAsync,
|
||||
} from '../../onboarding/installDependencies';
|
||||
import { runCommandAsync } from '../../onboarding/runCommand';
|
||||
import { PACKAGE_MANAGERS, PackageManager } from '../../onboarding/installDependencies';
|
||||
import { ora } from '../../ora';
|
||||
import { createOrModifyExpoConfigAsync, getPrivateExpoConfigAsync } from '../../project/expoConfig';
|
||||
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
|
||||
import { Choice, promptAsync } from '../../prompts';
|
||||
import { Actor, getActorUsername } from '../../user/User';
|
||||
import { easCliVersion } from '../../utils/easCli';
|
||||
import { Actor } from '../../user/User';
|
||||
|
||||
export async function promptForTargetDirectoryAsync(
|
||||
targetProjectDirFromArgs?: string
|
||||
): Promise<string> {
|
||||
Log.log(
|
||||
`🚚 Let's start by cloning the default Expo template project from GitHub and installing dependencies.`
|
||||
);
|
||||
Log.newLine();
|
||||
|
||||
if (targetProjectDirFromArgs) {
|
||||
return targetProjectDirFromArgs;
|
||||
}
|
||||
|
||||
const result = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'targetProjectDir',
|
||||
message: 'Where would you like to create your new project directory?',
|
||||
initial: path.join(process.cwd(), 'new-expo-project'),
|
||||
});
|
||||
|
||||
return result.targetProjectDir;
|
||||
}
|
||||
|
||||
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 cloneMethod = (await canAccessRepositoryUsingSshAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
}))
|
||||
? 'ssh'
|
||||
: 'https';
|
||||
Log.log(chalk.dim(`We detected that ${cloneMethod} is your preferred git clone method`));
|
||||
Log.newLine();
|
||||
|
||||
const { targetProjectDir: finalTargetProjectDirectory } = await runGitCloneAsync({
|
||||
githubUsername,
|
||||
githubRepositoryName,
|
||||
targetProjectDir,
|
||||
cloneMethod,
|
||||
});
|
||||
|
||||
return finalTargetProjectDirectory;
|
||||
}
|
||||
|
||||
export async function installProjectDependenciesAsync(projectDir: string): Promise<PackageManager> {
|
||||
const packageManager = await promptForPackageManagerAsync();
|
||||
await installDependenciesAsync({
|
||||
projectDir,
|
||||
packageManager,
|
||||
});
|
||||
|
||||
const dependencies = ['expo-updates', '@expo/metro-runtime'];
|
||||
for (const dependency of dependencies) {
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'npx',
|
||||
args: ['expo', 'install', dependency],
|
||||
});
|
||||
}
|
||||
return packageManager;
|
||||
}
|
||||
|
||||
export function getAccountChoices(
|
||||
export async function generateConfigsAsync(
|
||||
args: { path?: string },
|
||||
actor: Actor,
|
||||
namesWithSufficientPermissions: Set<string>
|
||||
): Choice[] {
|
||||
const sortedAccounts = actor.accounts.sort((a, _b) =>
|
||||
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
|
||||
);
|
||||
|
||||
return sortedAccounts.map(account => {
|
||||
const isPersonalAccount = actor.__typename === 'User' && account.name === actor.username;
|
||||
const accountDisplayName = isPersonalAccount
|
||||
? `${account.name} (personal account)`
|
||||
: account.name;
|
||||
const disabled = !namesWithSufficientPermissions.has(account.name);
|
||||
|
||||
return {
|
||||
title: accountDisplayName,
|
||||
value: { name: account.name },
|
||||
...(disabled && {
|
||||
disabled: true,
|
||||
description: 'You do not have the required permissions to create projects on this account.',
|
||||
}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function createProjectAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
actor: Actor,
|
||||
projectDir: string
|
||||
): Promise<string> {
|
||||
const allAccounts = actor.accounts;
|
||||
const accountNamesWhereUserHasSufficientPermissionsToCreateApp = new Set(
|
||||
allAccounts
|
||||
.filter(a => a.users.find(it => it.actor.id === actor.id)?.role !== Role.ViewOnly)
|
||||
.map(it => it.name)
|
||||
);
|
||||
|
||||
let accountName = allAccounts[0].name;
|
||||
if (allAccounts.length > 1) {
|
||||
const choices = getAccountChoices(
|
||||
actor,
|
||||
accountNamesWhereUserHasSufficientPermissionsToCreateApp
|
||||
);
|
||||
|
||||
accountName = (
|
||||
await promptAsync({
|
||||
type: 'select',
|
||||
name: 'account',
|
||||
message: 'Which account should own this project?',
|
||||
choices,
|
||||
})
|
||||
).account.name;
|
||||
}
|
||||
|
||||
const projectName = getActorUsername(actor) + '-app';
|
||||
const projectFullName = `@${accountName}/${projectName}`;
|
||||
const existingProjectIdOnServer = await findProjectIdByAccountNameAndSlugNullableAsync(
|
||||
graphqlClient: ExpoGraphqlClient
|
||||
): Promise<{
|
||||
projectName: string;
|
||||
projectDirectory: string;
|
||||
projectAccount: string;
|
||||
}> {
|
||||
const projectAccount = await promptForProjectAccountAsync(actor);
|
||||
const { projectName, projectDirectory } = await generateProjectConfigAsync(actor, args.path, {
|
||||
graphqlClient,
|
||||
accountName,
|
||||
projectName
|
||||
);
|
||||
projectAccount,
|
||||
});
|
||||
|
||||
if (existingProjectIdOnServer) {
|
||||
throw new Error(
|
||||
`Existing project found: ${projectFullName} (ID: ${existingProjectIdOnServer}). Project ID configuration canceled. Re-run the command to select a different account/project.`
|
||||
);
|
||||
}
|
||||
return {
|
||||
projectAccount,
|
||||
projectDirectory,
|
||||
projectName,
|
||||
};
|
||||
}
|
||||
|
||||
if (!accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(accountName)) {
|
||||
throw new Error(
|
||||
`You don't have permission to create a new project on the ${accountName} account and no matching project already exists on the account.`
|
||||
);
|
||||
}
|
||||
|
||||
const projectDashboardUrl = getProjectDashboardUrl(accountName, projectName);
|
||||
export async function createProjectAsync({
|
||||
graphqlClient,
|
||||
actor,
|
||||
projectDirectory,
|
||||
projectAccount,
|
||||
projectName,
|
||||
}: {
|
||||
graphqlClient: ExpoGraphqlClient;
|
||||
actor: Actor;
|
||||
projectDirectory: string;
|
||||
projectAccount: string;
|
||||
projectName: string;
|
||||
}): Promise<string> {
|
||||
const projectFullName = `@${projectAccount}/${projectName}`;
|
||||
const projectDashboardUrl = getProjectDashboardUrl(projectAccount, projectName);
|
||||
const projectLink = link(projectDashboardUrl, { text: projectFullName });
|
||||
|
||||
const account = nullthrows(allAccounts.find(a => a.name === accountName));
|
||||
const account = nullthrows(actor.accounts.find(a => a.name === projectAccount));
|
||||
|
||||
const spinner = ora(`Creating ${chalk.bold(projectFullName)}`).start();
|
||||
let projectId: string;
|
||||
@@ -187,9 +84,9 @@ export async function createProjectAsync(
|
||||
throw err;
|
||||
}
|
||||
|
||||
const exp = await getPrivateExpoConfigAsync(projectDir, { skipPlugins: true });
|
||||
const exp = await getPrivateExpoConfigAsync(projectDirectory, { skipPlugins: true });
|
||||
await createOrModifyExpoConfigAsync(
|
||||
projectDir,
|
||||
projectDirectory,
|
||||
{
|
||||
extra: { ...exp.extra, eas: { ...exp.extra?.eas, projectId } },
|
||||
},
|
||||
@@ -200,13 +97,10 @@ export async function createProjectAsync(
|
||||
return projectId;
|
||||
}
|
||||
|
||||
export function stripInvalidCharactersForBundleIdentifier(string: string): string {
|
||||
return string.replaceAll(/[^A-Za-z0-9]/g, '');
|
||||
}
|
||||
|
||||
export async function generateConfigFilesAsync(
|
||||
export async function generateProjectFilesAsync(
|
||||
projectDir: string,
|
||||
app: AppFragment
|
||||
app: AppFragment,
|
||||
packageManager: PackageManager
|
||||
): Promise<void> {
|
||||
await generateAppConfigAsync(projectDir, app);
|
||||
|
||||
@@ -216,206 +110,40 @@ export async function generateConfigFilesAsync(
|
||||
|
||||
await copyProjectTemplatesAsync(projectDir);
|
||||
|
||||
await mergeReadmeAsync(projectDir);
|
||||
await updateReadmeAsync(projectDir, packageManager);
|
||||
}
|
||||
|
||||
export async function generateAppConfigAsync(projectDir: string, app: AppFragment): Promise<void> {
|
||||
// Android package name requires each component to start with a lowercase letter.
|
||||
const isUsernameValidSegment = /^[^a-z]/.test(app.ownerAccount.name);
|
||||
const userPrefix = isUsernameValidSegment ? 'user' : '';
|
||||
const isSlugValidSegment = /^[^a-z]/.test(app.slug);
|
||||
const slugPrefix = isSlugValidSegment ? 'app' : '';
|
||||
|
||||
const bundleIdentifier = `com.${userPrefix}${stripInvalidCharactersForBundleIdentifier(
|
||||
app.ownerAccount.name
|
||||
)}.${slugPrefix}${stripInvalidCharactersForBundleIdentifier(app.slug)}`;
|
||||
const updateUrl = getEASUpdateURL(app.id, /* manifestHostOverride */ null);
|
||||
|
||||
const { expo: baseExpoConfig } = await fs.readJson(path.join(projectDir, 'app.json'));
|
||||
const expoConfig: ExpoConfig = {
|
||||
name: app.name ?? app.slug,
|
||||
slug: app.slug,
|
||||
scheme: stripInvalidCharactersForBundleIdentifier(app.name ?? app.slug),
|
||||
extra: {
|
||||
eas: {
|
||||
projectId: app.id,
|
||||
},
|
||||
},
|
||||
owner: app.ownerAccount.name,
|
||||
updates: {
|
||||
url: updateUrl,
|
||||
},
|
||||
runtimeVersion: {
|
||||
policy: 'appVersion',
|
||||
},
|
||||
ios: {
|
||||
bundleIdentifier,
|
||||
},
|
||||
android: {
|
||||
package: bundleIdentifier,
|
||||
},
|
||||
};
|
||||
const mergedConfig = merge(baseExpoConfig, expoConfig);
|
||||
|
||||
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> {
|
||||
const easBuildGitHubConfig = {
|
||||
android: {
|
||||
image: 'latest',
|
||||
},
|
||||
ios: {
|
||||
image: 'latest',
|
||||
},
|
||||
};
|
||||
|
||||
const easJson: EasJson = {
|
||||
cli: {
|
||||
version: `>= ${easCliVersion}`,
|
||||
appVersionSource: AppVersionSource.REMOTE,
|
||||
},
|
||||
build: {
|
||||
development: {
|
||||
developmentClient: true,
|
||||
distribution: 'internal',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
'development-simulator': {
|
||||
extends: 'development',
|
||||
ios: {
|
||||
simulator: true,
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
distribution: 'internal',
|
||||
channel: 'main',
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
production: {
|
||||
channel: 'production',
|
||||
autoIncrement: true,
|
||||
...easBuildGitHubConfig,
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
production: {},
|
||||
},
|
||||
};
|
||||
|
||||
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> {
|
||||
const packageJsonPath = path.join(projectDir, 'package.json');
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
|
||||
if (!packageJson.scripts) {
|
||||
packageJson.scripts = {};
|
||||
}
|
||||
packageJson.scripts.preview = 'npx eas-cli@latest workflow:run publish-preview-update.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');
|
||||
|
||||
await fs.copy(templatesSourceDir, easWorkflowsTargetDir, {
|
||||
overwrite: true,
|
||||
errorOnExist: false,
|
||||
});
|
||||
|
||||
Log.withTick('Created EAS workflow files');
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function mergeReadmeAsync(projectDir: string): Promise<void> {
|
||||
const readmeTemplatePath = path.join(__dirname, 'templates', 'readme-additions.md');
|
||||
const projectReadmePath = path.join(projectDir, 'README.md');
|
||||
|
||||
const readmeAdditions = await fs.readFile(readmeTemplatePath, 'utf8');
|
||||
const existingReadme = await fs.readFile(projectReadmePath, 'utf8');
|
||||
|
||||
const targetSection = '## Get a fresh project';
|
||||
const sectionIndex = existingReadme.indexOf(targetSection);
|
||||
|
||||
let mergedReadme: string;
|
||||
if (sectionIndex !== -1) {
|
||||
// Insert before "## Get a fresh project" section
|
||||
const beforeSection = existingReadme.substring(0, sectionIndex).trim();
|
||||
const afterSection = existingReadme.substring(sectionIndex);
|
||||
mergedReadme = beforeSection + '\n\n' + readmeAdditions.trim() + '\n\n' + afterSection;
|
||||
} else {
|
||||
// Append to the end if section doesn't exist
|
||||
mergedReadme = existingReadme.trim() + '\n\n' + readmeAdditions.trim() + '\n';
|
||||
}
|
||||
|
||||
await fs.writeFile(projectReadmePath, mergedReadme);
|
||||
|
||||
Log.withTick('Updated README.md with EAS configuration details');
|
||||
Log.log();
|
||||
}
|
||||
|
||||
export async function initializeGitRepositoryAsync(projectDir: string): Promise<void> {
|
||||
await fs.remove(path.join(projectDir, '.git'));
|
||||
|
||||
const commands = [['init'], ['add', '.'], ['commit', '-m', 'Initial commit']];
|
||||
|
||||
for (const args of commands) {
|
||||
await runCommandAsync({
|
||||
cwd: projectDir,
|
||||
command: 'git',
|
||||
args,
|
||||
});
|
||||
Log.log();
|
||||
}
|
||||
}
|
||||
|
||||
export const formatScriptCommand = (script: string, packageManager: PackageManager): string => {
|
||||
if (packageManager === 'npm') {
|
||||
return `npm run ${script}`;
|
||||
}
|
||||
return `${packageManager} ${script}`;
|
||||
};
|
||||
|
||||
export default class New extends EasCommand {
|
||||
static override aliases = ['new'];
|
||||
|
||||
static override description = "create a new project set up with Expo's services.";
|
||||
static override description =
|
||||
'Create a new project configured with Expo Application Services (EAS)';
|
||||
|
||||
static override flags = {};
|
||||
static override args = [
|
||||
{
|
||||
name: 'path',
|
||||
description: 'Path to create the project (defaults to current directory)',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
static override flags = {
|
||||
'package-manager': Flags.enum<PackageManager>({
|
||||
char: 'p',
|
||||
description: 'Package manager to use for installing dependencies',
|
||||
options: [...PACKAGE_MANAGERS],
|
||||
default: 'npm',
|
||||
}),
|
||||
};
|
||||
|
||||
static override hidden = true;
|
||||
|
||||
static override args = [{ name: 'TARGET_PROJECT_DIRECTORY' }];
|
||||
|
||||
static override contextDefinition = {
|
||||
...this.ContextOptions.LoggedIn,
|
||||
};
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const { args } = await this.parse(New);
|
||||
const { args, flags } = await this.parse(New);
|
||||
|
||||
const {
|
||||
loggedIn: { actor, graphqlClient },
|
||||
@@ -428,22 +156,32 @@ export default class New extends EasCommand {
|
||||
}
|
||||
|
||||
Log.warn(
|
||||
'This command is not yet implemented. It will create a new project, but it will not be fully configured.'
|
||||
'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 targetProjectDirectory = await promptForTargetDirectoryAsync(
|
||||
args.TARGET_PROJECT_DIRECTORY
|
||||
);
|
||||
const {
|
||||
projectName,
|
||||
projectDirectory: targetProjectDirectory,
|
||||
projectAccount,
|
||||
} = await generateConfigsAsync(args, actor, graphqlClient);
|
||||
|
||||
const projectDirectory = await cloneTemplateAsync(targetProjectDirectory);
|
||||
|
||||
const packageManager = await installProjectDependenciesAsync(projectDirectory);
|
||||
const packageManager = flags['package-manager'];
|
||||
await installProjectDependenciesAsync(projectDirectory, packageManager);
|
||||
|
||||
const projectId = await createProjectAsync(graphqlClient, actor, projectDirectory);
|
||||
const projectId = await createProjectAsync({
|
||||
projectDirectory,
|
||||
projectAccount,
|
||||
projectName,
|
||||
actor,
|
||||
graphqlClient,
|
||||
});
|
||||
|
||||
const app = await AppQuery.byIdAsync(graphqlClient, projectId);
|
||||
await generateConfigFilesAsync(projectDirectory, app);
|
||||
await generateProjectFilesAsync(projectDirectory, app, packageManager);
|
||||
|
||||
await initializeGitRepositoryAsync(projectDirectory);
|
||||
|
||||
@@ -451,18 +189,12 @@ export default class New extends EasCommand {
|
||||
Log.log('Next steps:');
|
||||
Log.withInfo(`Run \`cd ${projectDirectory}\` to navigate to your project.`);
|
||||
Log.withInfo(
|
||||
`Run \`${formatScriptCommand(
|
||||
'preview',
|
||||
packageManager
|
||||
)}\` to create a preview build on EAS. ${learnMore(
|
||||
`Run \`${packageManager} run preview\` to create a preview build on EAS. ${learnMore(
|
||||
'https://docs.expo.dev/eas/workflows/examples/publish-preview-update/'
|
||||
)}`
|
||||
);
|
||||
Log.withInfo(
|
||||
`Run \`${formatScriptCommand(
|
||||
'start',
|
||||
packageManager
|
||||
)}\` to start developing locally. ${learnMore(
|
||||
`Run \`${packageManager} run start\` to start developing locally. ${learnMore(
|
||||
'https://docs.expo.dev/get-started/start-developing/'
|
||||
)}`
|
||||
);
|
||||
|
||||
@@ -39,7 +39,7 @@ export default class UpdateConfigure extends EasCommand {
|
||||
vcsClient,
|
||||
} = await this.getContextAsync(UpdateConfigure, {
|
||||
nonInteractive: flags['non-interactive'],
|
||||
withServerSideEnvironment: flags['environment'],
|
||||
withServerSideEnvironment: flags['environment'] ?? null,
|
||||
});
|
||||
|
||||
Log.log(
|
||||
|
||||
@@ -6,7 +6,6 @@ import nullthrows from 'nullthrows';
|
||||
|
||||
// import { getExpoWebsiteBaseUrl } from '../../api';
|
||||
import { ensureBranchExistsAsync } from '../../branch/queries';
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import { ensureRepoIsCleanAsync } from '../../build/utils/repository';
|
||||
import { getUpdateGroupUrl } from '../../build/utils/url';
|
||||
import EasCommand from '../../commandUtils/EasCommand';
|
||||
@@ -79,7 +78,7 @@ type RawUpdateFlags = {
|
||||
'rollout-percentage'?: number;
|
||||
'non-interactive': boolean;
|
||||
json: boolean;
|
||||
environment: EnvironmentVariableEnvironment | null;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
type UpdateFlags = {
|
||||
@@ -96,7 +95,7 @@ type UpdateFlags = {
|
||||
rolloutPercentage?: number;
|
||||
json: boolean;
|
||||
nonInteractive: boolean;
|
||||
environment: EnvironmentVariableEnvironment | null;
|
||||
environment?: string;
|
||||
};
|
||||
|
||||
export default class UpdatePublish extends EasCommand {
|
||||
@@ -193,7 +192,7 @@ export default class UpdatePublish extends EasCommand {
|
||||
getServerSideEnvironmentVariablesAsync,
|
||||
} = await this.getContextAsync(UpdatePublish, {
|
||||
nonInteractive,
|
||||
withServerSideEnvironment: environment,
|
||||
withServerSideEnvironment: environment ?? null,
|
||||
});
|
||||
|
||||
if (jsonFlag) {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { parseInputs, parseJsonInputs, parseWorkflowInputsFromYaml } from '../run';
|
||||
import {
|
||||
parseInputs,
|
||||
parseJsonInputs,
|
||||
parseWorkflowInputsFromYaml,
|
||||
} from '../../../commandUtils/workflow/inputs';
|
||||
|
||||
describe('parseInputs', () => {
|
||||
it('should parse single key=value pair', () => {
|
||||
@@ -216,7 +220,6 @@ jobs:
|
||||
it('should handle invalid YAML gracefully', () => {
|
||||
const yamlConfig = 'invalid: yaml: content:';
|
||||
|
||||
const { parseWorkflowInputsFromYaml } = require('../run');
|
||||
const inputs = parseWorkflowInputsFromYaml(yamlConfig);
|
||||
|
||||
expect(inputs).toEqual({});
|
||||
|
||||
@@ -44,16 +44,25 @@ import spawnAsync from '@expo/spawn-async';
|
||||
import { Flags } from '@oclif/core';
|
||||
import { CombinedError } from '@urql/core';
|
||||
import chalk from 'chalk';
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import slash from 'slash';
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
|
||||
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,
|
||||
parseInputs,
|
||||
parseJsonInputs,
|
||||
parseWorkflowInputsFromYaml,
|
||||
} from '../../commandUtils/workflow/inputs';
|
||||
import {
|
||||
fileExistsAsync,
|
||||
infoForActiveWorkflowRunAsync,
|
||||
infoForFailedWorkflowRunAsync,
|
||||
maybeReadStdinAsync,
|
||||
} from '../../commandUtils/workflow/utils';
|
||||
import {
|
||||
WorkflowProjectSourceType,
|
||||
WorkflowRevision,
|
||||
@@ -68,7 +77,6 @@ import { ora } from '../../ora';
|
||||
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
|
||||
import { uploadAccountScopedFileAsync } from '../../project/uploadAccountScopedFileAsync';
|
||||
import { uploadAccountScopedProjectSourceAsync } from '../../project/uploadAccountScopedProjectSourceAsync';
|
||||
import { promptAsync } from '../../prompts';
|
||||
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
|
||||
import { sleepAsync } from '../../utils/promise';
|
||||
import { WorkflowFile } from '../../utils/workflowFile';
|
||||
@@ -91,7 +99,8 @@ 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',
|
||||
summary:
|
||||
'Wait for workflow run to complete. Defaults to false. Cannot be used with the --json flag.',
|
||||
}),
|
||||
input: Flags.string({
|
||||
char: 'F',
|
||||
@@ -120,6 +129,9 @@ 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();
|
||||
}
|
||||
|
||||
@@ -388,36 +400,45 @@ async function waitForWorkflowRunToEndAsync(
|
||||
): Promise<WorkflowRunByIdQuery['workflowRuns']['byId']> {
|
||||
Log.log('Waiting for workflow run to complete. You can press Ctrl+C to exit.');
|
||||
|
||||
const spinner = ora('Currently waiting for workflow run to start.').start();
|
||||
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.byIdAsync(graphqlClient, workflowRunId, {
|
||||
const workflowRun = await WorkflowRunQuery.withJobsByIdAsync(graphqlClient, workflowRunId, {
|
||||
useCache: false,
|
||||
});
|
||||
|
||||
failedFetchesCount = 0;
|
||||
|
||||
switch (workflowRun.status) {
|
||||
case WorkflowRunStatus.InProgress:
|
||||
spinner.start('Workflow run is in progress.');
|
||||
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.warn('Workflow run is waiting for action.');
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow run is waiting for action:}`;
|
||||
break;
|
||||
|
||||
case WorkflowRunStatus.Canceled:
|
||||
spinner.warn('Workflow run has been canceled.');
|
||||
spinner.prefixText = chalk`{bold.yellow Workflow has been canceled.}`;
|
||||
spinner.stopAndPersist();
|
||||
return workflowRun;
|
||||
|
||||
case WorkflowRunStatus.Failure:
|
||||
spinner.fail('Workflow run has failed.');
|
||||
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.succeed('Workflow run completed successfully.');
|
||||
spinner.prefixText = chalk`{bold.green Workflow has completed successfully.}`;
|
||||
spinner.succeed('');
|
||||
return workflowRun;
|
||||
}
|
||||
} catch {
|
||||
@@ -434,257 +455,3 @@ async function waitForWorkflowRunToEndAsync(
|
||||
await sleepAsync(10 /* seconds */ * 1000 /* milliseconds */);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExistsAsync(filePath: string): Promise<boolean> {
|
||||
return await fs.promises
|
||||
.access(filePath, fs.constants.F_OK)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
export function parseInputs(inputFlags: string[]): Record<string, string> {
|
||||
const inputs: Record<string, string> = {};
|
||||
|
||||
for (const inputFlag of inputFlags) {
|
||||
const equalIndex = inputFlag.indexOf('=');
|
||||
if (equalIndex === -1) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Expected key=value format.`);
|
||||
}
|
||||
|
||||
const key = inputFlag.substring(0, equalIndex);
|
||||
const value = inputFlag.substring(equalIndex + 1);
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`Invalid input format: ${inputFlag}. Key cannot be empty.`);
|
||||
}
|
||||
|
||||
inputs[key] = value;
|
||||
}
|
||||
|
||||
return inputs;
|
||||
}
|
||||
|
||||
export async function maybeReadStdinAsync(): Promise<string | null> {
|
||||
// Check if there's data on stdin
|
||||
if (process.stdin.isTTY) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
process.stdin.setEncoding('utf8');
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
let chunk;
|
||||
while ((chunk = process.stdin.read()) !== null) {
|
||||
data += chunk;
|
||||
}
|
||||
});
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
const trimmedData = data.trim();
|
||||
resolve(trimmedData || null);
|
||||
});
|
||||
|
||||
process.stdin.on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function parseJsonInputs(jsonString: string): Record<string, string> {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonString);
|
||||
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
throw new Error('JSON input must be an object.');
|
||||
}
|
||||
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input.`, { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
|
||||
// which is not what we want. See:
|
||||
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
|
||||
const booleanLike = z.union([
|
||||
z.boolean(),
|
||||
z.codec(z.number(), z.boolean(), {
|
||||
decode: n => !!n,
|
||||
encode: b => (b ? 1 : 0),
|
||||
}),
|
||||
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
|
||||
]);
|
||||
|
||||
const stringLike = z.codec(
|
||||
z.union([
|
||||
// We're going to coerce numbers and strings into strings.
|
||||
z.number(),
|
||||
z.string(),
|
||||
// We do not allow other primitives, like:
|
||||
// - bigints, symbols - because YAML does not support them,
|
||||
// - booleans - because YAML accepts `True` and `true` as boolean input
|
||||
// and parses both as JS `true` -- if we stringified that,
|
||||
// we would lose the capital "T" which may not be what the user expects,
|
||||
// - nulls - user should do `"null"` or not pass the property at all.
|
||||
]),
|
||||
z.string(),
|
||||
{
|
||||
decode: value => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
|
||||
},
|
||||
encode: value => value,
|
||||
}
|
||||
);
|
||||
|
||||
export const WorkflowDispatchInputZ = z
|
||||
.object({
|
||||
description: stringLike.optional().describe('Description of the input'),
|
||||
required: booleanLike.default(false).describe('Whether the input is required.'),
|
||||
})
|
||||
.and(
|
||||
z.union([
|
||||
z.object({
|
||||
type: z.literal('string').default('string'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('boolean'),
|
||||
default: booleanLike.optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('number'),
|
||||
default: z.number().optional().describe('Default value for the input'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('choice'),
|
||||
default: stringLike.optional().describe('Default value for the input'),
|
||||
options: z.array(stringLike).min(1).describe('Options for choice type inputs'),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('environment'),
|
||||
default: z.string().optional().describe('Default value for the input'),
|
||||
}),
|
||||
])
|
||||
);
|
||||
|
||||
export function parseWorkflowInputsFromYaml(
|
||||
yamlConfig: string
|
||||
): Record<string, z.infer<typeof WorkflowDispatchInputZ>> {
|
||||
try {
|
||||
const parsed = YAML.parse(yamlConfig);
|
||||
return z
|
||||
.record(z.string(), WorkflowDispatchInputZ)
|
||||
.default({})
|
||||
.parse(parsed?.on?.workflow_dispatch?.inputs);
|
||||
} catch (error) {
|
||||
Log.warn('Failed to parse workflow inputs from YAML:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
async function maybePromptForMissingInputsAsync({
|
||||
inputSpecs,
|
||||
inputs,
|
||||
}: {
|
||||
inputSpecs: Record<string, z.infer<typeof WorkflowDispatchInputZ>>;
|
||||
inputs: Record<string, unknown>;
|
||||
}): Promise<Record<string, unknown>> {
|
||||
const requiredInputs = Object.entries(inputSpecs).filter(([_, spec]) => spec.required);
|
||||
|
||||
const missingRequiredInputs = requiredInputs.filter(([key]) => inputs[key] === undefined);
|
||||
|
||||
if (missingRequiredInputs.length === 0) {
|
||||
return inputs;
|
||||
}
|
||||
|
||||
Log.addNewLineIfNone();
|
||||
Log.log('Some required inputs are missing. Please provide them:');
|
||||
|
||||
const nextInputs = { ...inputs };
|
||||
|
||||
for (const [key, spec] of missingRequiredInputs) {
|
||||
const value = await promptForMissingInputAsync({ key, spec });
|
||||
nextInputs[key] = value;
|
||||
}
|
||||
|
||||
return nextInputs;
|
||||
}
|
||||
|
||||
async function promptForMissingInputAsync({
|
||||
key,
|
||||
spec,
|
||||
}: {
|
||||
key: string;
|
||||
spec: z.infer<typeof WorkflowDispatchInputZ>;
|
||||
}): Promise<unknown> {
|
||||
const message = spec.description ? `${key} (${spec.description})` : key;
|
||||
|
||||
switch (spec.type) {
|
||||
case 'boolean': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'confirm',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'number': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'number',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: number) => {
|
||||
if (isNaN(val)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'choice': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'select',
|
||||
name: 'value',
|
||||
message,
|
||||
choices: spec.options.map(option => ({
|
||||
title: option,
|
||||
value: option,
|
||||
})),
|
||||
initial: spec.default,
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
case 'string':
|
||||
case 'environment': {
|
||||
const { value } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'value',
|
||||
message,
|
||||
initial: spec.default,
|
||||
validate: (val: string) => {
|
||||
if (spec.required && (!val || val.trim() === '')) {
|
||||
return 'This field is required';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { JSONObject } from '@expo/json-file';
|
||||
import type { XCBuildConfiguration } from 'xcode';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
AccountFragment,
|
||||
@@ -33,3 +34,42 @@ export type IosCredentials = Record<string, TargetCredentials>;
|
||||
|
||||
export type IosAppBuildCredentialsMap = Record<string, IosAppBuildCredentialsFragment>;
|
||||
export type IosAppCredentialsMap = Record<string, CommonIosAppCredentialsFragment | null>;
|
||||
// `z.coerce.boolean()` does `Boolean(val)` under the hood,
|
||||
// which is not what we want. See:
|
||||
// https://github.com/colinhacks/zod/issues/2985#issuecomment-2230692578
|
||||
|
||||
export const booleanLike = z.union([
|
||||
z.boolean(),
|
||||
z.codec(z.number(), z.boolean(), {
|
||||
decode: n => !!n,
|
||||
encode: b => (b ? 1 : 0),
|
||||
}),
|
||||
z.stringbool({ truthy: ['true', 'True'], falsy: ['false', 'False'] }),
|
||||
]);
|
||||
|
||||
export const stringLike = z.codec(
|
||||
z.union([
|
||||
// We're going to coerce numbers and strings into strings.
|
||||
z.number(),
|
||||
z.string(),
|
||||
// We do not allow other primitives, like:
|
||||
// - bigints, symbols - because YAML does not support them,
|
||||
// - booleans - because YAML accepts `True` and `true` as boolean input
|
||||
// and parses both as JS `true` -- if we stringified that,
|
||||
// we would lose the capital "T" which may not be what the user expects,
|
||||
// - nulls - user should do `"null"` or not pass the property at all.
|
||||
]),
|
||||
z.string(),
|
||||
{
|
||||
decode: value => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'number') {
|
||||
return String(value);
|
||||
}
|
||||
throw new Error(`Cannot convert ${typeof value} to string: ${value}`);
|
||||
},
|
||||
encode: value => value,
|
||||
}
|
||||
);
|
||||
+98
-17
@@ -188,6 +188,8 @@ export type Account = {
|
||||
userInvitations: Array<UserInvitation>;
|
||||
/** Actors associated with this account and permissions they hold */
|
||||
users: Array<UserPermission>;
|
||||
/** Notification preferences of the viewer for this account */
|
||||
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
|
||||
/** Permission info for the viewer on this account */
|
||||
viewerUserPermission: UserPermission;
|
||||
/** @deprecated Build packs are no longer supported */
|
||||
@@ -647,6 +649,22 @@ export type AccountMutationSetPushSecurityEnabledArgs = {
|
||||
pushSecurityEnabled: Scalars['Boolean']['input'];
|
||||
};
|
||||
|
||||
/** Account-level notification preference */
|
||||
export type AccountNotificationPreference = NotificationPreference & {
|
||||
__typename?: 'AccountNotificationPreference';
|
||||
account: Account;
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AccountNotificationPreferenceInput = {
|
||||
accountId: Scalars['ID']['input'];
|
||||
enabled: Scalars['Boolean']['input'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AccountNotificationSubscriptionInput = {
|
||||
accountId: Scalars['ID']['input'];
|
||||
event: NotificationEvent;
|
||||
@@ -1416,6 +1434,8 @@ export type App = Project & {
|
||||
username: Scalars['String']['output'];
|
||||
/** @deprecated No longer supported */
|
||||
users?: Maybe<Array<Maybe<User>>>;
|
||||
/** Notification preferences of the viewer for this app */
|
||||
viewerNotificationPreferences: Array<NotificationPreferenceItem>;
|
||||
/** Webhooks for an app */
|
||||
webhooks: Array<Webhook>;
|
||||
workerCustomDomain?: Maybe<WorkerCustomDomain>;
|
||||
@@ -1918,6 +1938,22 @@ export type AppMutationSetResourceClassExperimentArgs = {
|
||||
resourceClassExperiment?: InputMaybe<ResourceClassExperiment>;
|
||||
};
|
||||
|
||||
/** App-level notification preference */
|
||||
export type AppNotificationPreference = NotificationPreference & {
|
||||
__typename?: 'AppNotificationPreference';
|
||||
app: App;
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AppNotificationPreferenceInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
enabled: Scalars['Boolean']['input'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type AppNotificationSubscriptionInput = {
|
||||
appId: Scalars['ID']['input'];
|
||||
event: NotificationEvent;
|
||||
@@ -5613,7 +5649,10 @@ export type MeMutation = {
|
||||
scheduleCurrentUserDeletion: BackgroundJobReceipt;
|
||||
/** Schedule deletion of a SSO user. Actor must be an owner on the SSO user's SSO account. */
|
||||
scheduleSSOUserDeletionAsSSOAccountOwner: BackgroundJobReceipt;
|
||||
/** Send SMS OTP to a second factor device for use during device setup or during change confirmation */
|
||||
/**
|
||||
* Send SMS OTP to a second factor device for use during device setup or during change confirmation
|
||||
* @deprecated This method is no longer supported.
|
||||
*/
|
||||
sendSMSOTPToSecondFactorDevice: SecondFactorBooleanResult;
|
||||
/**
|
||||
* Sets user preferences. This is a key-value store for user-specific settings. Provided values are
|
||||
@@ -5757,6 +5796,32 @@ export enum NotificationEvent {
|
||||
|
||||
export type NotificationMetadata = BuildLimitThresholdExceededMetadata | BuildPlanCreditThresholdExceededMetadata | TestNotificationMetadata;
|
||||
|
||||
/** Base interface for notification preferences */
|
||||
export type NotificationPreference = {
|
||||
enabled: Scalars['Boolean']['output'];
|
||||
event: NotificationEvent;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
/** Union type representing either account-level or app-level notification preferences */
|
||||
export type NotificationPreferenceItem = AccountNotificationPreference | AppNotificationPreference;
|
||||
|
||||
export type NotificationPreferenceMutation = {
|
||||
__typename?: 'NotificationPreferenceMutation';
|
||||
setAccountNotificationPreference: NotificationPreferenceItem;
|
||||
setAppNotificationPreference: NotificationPreferenceItem;
|
||||
};
|
||||
|
||||
|
||||
export type NotificationPreferenceMutationSetAccountNotificationPreferenceArgs = {
|
||||
input: AccountNotificationPreferenceInput;
|
||||
};
|
||||
|
||||
|
||||
export type NotificationPreferenceMutationSetAppNotificationPreferenceArgs = {
|
||||
input: AppNotificationPreferenceInput;
|
||||
};
|
||||
|
||||
export type NotificationSubscription = {
|
||||
__typename?: 'NotificationSubscription';
|
||||
account?: Maybe<Account>;
|
||||
@@ -6232,7 +6297,12 @@ export type RootMutation = {
|
||||
logRocketProject: LogRocketProjectMutation;
|
||||
/** Mutations that modify the currently authenticated User */
|
||||
me: MeMutation;
|
||||
/** Mutations that modify a NotificationSubscription */
|
||||
/** Notification preference management */
|
||||
notificationPreference: NotificationPreferenceMutation;
|
||||
/**
|
||||
* Mutations that modify a NotificationSubscription
|
||||
* @deprecated Use notificationPreference mutation instead
|
||||
*/
|
||||
notificationSubscription: NotificationSubscriptionMutation;
|
||||
/** Mutations that create, update, and delete Robots */
|
||||
robot: RobotMutation;
|
||||
@@ -6573,6 +6643,7 @@ export type SsoUser = Actor & UserActor & {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
pinnedApps: Array<App>;
|
||||
pinnedDashboardViews: Array<PinnedDashboardView>;
|
||||
@@ -6928,6 +6999,7 @@ export type Submission = ActivityTimelineProjectActivity & {
|
||||
id: Scalars['ID']['output'];
|
||||
initiatingActor?: Maybe<Actor>;
|
||||
iosConfig?: Maybe<IosSubmissionConfig>;
|
||||
jobRun?: Maybe<JobRun>;
|
||||
logFiles: Array<Scalars['String']['output']>;
|
||||
/** @deprecated Use logFiles instead */
|
||||
logsUrl?: Maybe<Scalars['String']['output']>;
|
||||
@@ -7619,6 +7691,7 @@ export type User = Actor & UserActor & {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
/** Pending UserInvitations for this user. Only resolves for the viewer. */
|
||||
pendingUserInvitations: Array<UserInvitation>;
|
||||
@@ -7731,6 +7804,7 @@ export type UserActor = {
|
||||
lastName?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated No longer supported */
|
||||
location?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated Use Account/App.viewerNotificationPreferences instead */
|
||||
notificationSubscriptions: Array<NotificationSubscription>;
|
||||
pinnedApps: Array<App>;
|
||||
preferences: UserPreferences;
|
||||
@@ -9653,7 +9727,7 @@ export type CreateAndroidBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateAndroidBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createAndroidBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type CreateIosBuildMutationVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -9663,7 +9737,7 @@ export type CreateIosBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
export type CreateIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createIosBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }, deprecationInfo?: { __typename?: 'EASBuildDeprecationInfo', type: EasBuildDeprecationInfoType, message: string } | null } } };
|
||||
|
||||
export type UpdateBuildMetadataMutationVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
@@ -9671,7 +9745,7 @@ export type UpdateBuildMetadataMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type UpdateBuildMetadataMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', updateBuildMetadata: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type RetryIosBuildMutationVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
@@ -9679,7 +9753,7 @@ export type RetryIosBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type RetryIosBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', retryIosBuild: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type CreateEnvironmentSecretForAccountMutationVariables = Exact<{
|
||||
input: CreateEnvironmentSecretInput;
|
||||
@@ -9763,7 +9837,7 @@ export type CreateLocalBuildMutationVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
|
||||
export type CreateLocalBuildMutation = { __typename?: 'RootMutation', build: { __typename?: 'BuildMutation', createLocalBuild: { __typename?: 'CreateBuildResult', build: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } } };
|
||||
|
||||
export type GetSignedUploadMutationVariables = Exact<{
|
||||
contentTypes: Array<Scalars['String']['input']> | Scalars['String']['input'];
|
||||
@@ -10009,21 +10083,21 @@ export type BuildsByIdQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type BuildsWithSubmissionsByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsWithSubmissionsByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type BuildsWithFingerprintByIdQueryVariables = Exact<{
|
||||
buildId: Scalars['ID']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
export type BuildsWithFingerprintByIdQuery = { __typename?: 'RootQuery', builds: { __typename?: 'BuildQuery', byId: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } } };
|
||||
|
||||
export type ViewBuildsOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10033,7 +10107,7 @@ export type ViewBuildsOnAppQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
|
||||
export type ViewBuildsOnAppQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, builds: Array<{ __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null }> } } };
|
||||
|
||||
export type ViewUpdateChannelBasicInfoOnAppQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
@@ -10079,6 +10153,13 @@ export type EnvironmentSecretsByAppIdQueryVariables = Exact<{
|
||||
|
||||
export type EnvironmentSecretsByAppIdQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, ownerAccount: { __typename?: 'Account', id: string, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> }, environmentSecrets: Array<{ __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any }> } } };
|
||||
|
||||
export type AppEnvironmentVariableEnvironmentsQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
|
||||
export type AppEnvironmentVariableEnvironmentsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, environmentVariableEnvironments: Array<any> } } };
|
||||
|
||||
export type EnvironmentVariablesIncludingSensitiveByAppIdQueryVariables = Exact<{
|
||||
appId: Scalars['String']['input'];
|
||||
filterNames?: InputMaybe<Array<Scalars['String']['input']> | Scalars['String']['input']>;
|
||||
@@ -10250,7 +10331,7 @@ export type WorkflowJobByIdQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
|
||||
export type WorkflowJobByIdQuery = { __typename?: 'RootQuery', workflowJobs: { __typename?: 'WorkflowJobQuery', byId: { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> } } };
|
||||
|
||||
export type WorkflowRunByIdQueryVariables = Exact<{
|
||||
workflowRunId: Scalars['ID']['input'];
|
||||
@@ -10264,7 +10345,7 @@ export type WorkflowRunByIdWithJobsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
|
||||
export type WorkflowRunByIdWithJobsQuery = { __typename?: 'RootQuery', workflowRuns: { __typename?: 'WorkflowRunQuery', byId: { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, app: { __typename?: 'App', id: string, name: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } }, jobs: Array<{ __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> }>, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }> } } };
|
||||
|
||||
export type WorkflowRunsForAppIdFileNameAndStatusQueryVariables = Exact<{
|
||||
appId: Scalars['ID']['input'];
|
||||
@@ -10282,11 +10363,11 @@ export type AppFragment = { __typename?: 'App', id: string, name: string, fullNa
|
||||
|
||||
export type BackgroundJobReceiptDataFragment = { __typename?: 'BackgroundJobReceipt', id: string, state: BackgroundJobState, tries: number, willRetry: boolean, resultId?: string | null, resultType: BackgroundJobResultType, resultData?: any | null, errorCode?: string | null, errorMessage?: string | null, createdAt: any, updatedAt: any };
|
||||
|
||||
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildWithSubmissionsFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, submissions: Array<{ __typename?: 'Submission', id: string, status: SubmissionStatus, platform: AppPlatform, logFiles: Array<string>, app: { __typename?: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } }, androidConfig?: { __typename?: 'AndroidSubmissionConfig', applicationIdentifier?: string | null, track: string, releaseStatus?: SubmissionAndroidReleaseStatus | null, rollout?: number | null } | null, iosConfig?: { __typename?: 'IosSubmissionConfig', ascAppIdentifier: string, appleIdUsername?: string | null } | null, error?: { __typename?: 'SubmissionError', errorCode?: string | null, message?: string | null } | null }>, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
export type BuildWithFingerprintFragment = { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string, debugInfoUrl?: string | null, builds: { __typename?: 'AppBuildsConnection', edges: Array<{ __typename?: 'AppBuildEdge', node: { __typename?: 'Build', platform: AppPlatform, id: string } }> }, updates: { __typename?: 'AppUpdatesConnection', edges: Array<{ __typename?: 'AppUpdateEdge', node: { __typename?: 'Update', id: string, platform: string } }> } } | null, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null };
|
||||
|
||||
export type EnvironmentSecretFragment = { __typename?: 'EnvironmentSecret', id: string, name: string, type: EnvironmentSecretType, createdAt: any };
|
||||
|
||||
@@ -10314,7 +10395,7 @@ export type WebhookFragment = { __typename?: 'Webhook', id: string, event: Webho
|
||||
|
||||
export type WorkflowFragment = { __typename?: 'Workflow', id: string, name?: string | null, fileName: string, createdAt: any, updatedAt: any, revisionsPaginated: { __typename?: 'WorkflowRevisionsConnection', edges: Array<{ __typename?: 'WorkflowRevisionEdge', node: { __typename?: 'WorkflowRevision', id: string, blobSha: string, commitSha?: string | null, createdAt: any, yamlConfig: string } }> } };
|
||||
|
||||
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
|
||||
export type WorkflowJobFragment = { __typename?: 'WorkflowJob', id: string, key: string, name: string, status: WorkflowJobStatus, type: WorkflowJobType, outputs: any, createdAt: any, updatedAt: any, workflowRun: { __typename?: 'WorkflowRun', id: string }, turtleJobRun?: { __typename?: 'JobRun', id: string, logFileUrls: Array<string>, artifacts: Array<{ __typename?: 'WorkflowArtifact', id: string, name: string, contentType?: string | null, fileSizeBytes?: number | null, filename: string, downloadUrl?: string | null }>, errors: Array<{ __typename?: 'JobRunError', errorCode: string, message: string }> } | null, turtleBuild?: { __typename?: 'Build', id: string, status: BuildStatus, platform: AppPlatform, logFiles: Array<string>, channel?: string | null, distribution?: DistributionType | null, iosEnterpriseProvisioning?: BuildIosEnterpriseProvisioning | null, buildProfile?: string | null, sdkVersion?: string | null, appVersion?: string | null, appBuildVersion?: string | null, runtimeVersion?: string | null, gitCommitHash?: string | null, gitCommitMessage?: string | null, initialQueuePosition?: number | null, queuePosition?: number | null, estimatedWaitTimeLeftSeconds?: number | null, priority: BuildPriority, createdAt: any, updatedAt: any, message?: string | null, completedAt?: any | null, expirationDate?: any | null, isForIosSimulator: boolean, error?: { __typename?: 'BuildError', errorCode: string, message: string, docsUrl?: string | null } | null, artifacts?: { __typename?: 'BuildArtifacts', buildUrl?: string | null, xcodeBuildLogsUrl?: string | null, applicationArchiveUrl?: string | null, buildArtifactsUrl?: string | null } | null, fingerprint?: { __typename?: 'Fingerprint', id: string, hash: string } | null, initiatingActor?: { __typename: 'Robot', id: string, displayName: string } | { __typename: 'SSOUser', id: string, displayName: string } | { __typename: 'User', id: string, displayName: string } | null, project: { __typename: 'App', id: string, name: string, slug: string, ownerAccount: { __typename?: 'Account', id: string, name: string } } | { __typename: 'Snack', id: string, name: string, slug: string }, metrics?: { __typename?: 'BuildMetrics', buildWaitTime?: number | null, buildQueueTime?: number | null, buildDuration?: number | null } | null } | null, errors: Array<{ __typename?: 'WorkflowJobError', title: string, message: string }> };
|
||||
|
||||
export type WorkflowRunFragment = { __typename?: 'WorkflowRun', id: string, status: WorkflowRunStatus, gitCommitMessage?: string | null, gitCommitHash?: string | null, requestedGitRef?: string | null, triggeringLabelName?: string | null, triggerEventType: WorkflowRunTriggerEventType, triggeringSchedule?: string | null, createdAt: any, updatedAt: any, actor?: { __typename: 'Robot', firstName?: string | null, id: string } | { __typename: 'SSOUser', username: string, id: string } | { __typename: 'User', username: string, id: string } | null, errors: Array<{ __typename?: 'WorkflowRunError', title?: string | null, message: string }>, workflow: { __typename?: 'Workflow', id: string, name?: string | null, fileName: string } };
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { print } from 'graphql';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { withErrorHandlingAsync } from '../client';
|
||||
import {
|
||||
@@ -14,7 +13,7 @@ import { EnvironmentVariableFragmentNode } from '../types/EnvironmentVariable';
|
||||
import { EnvironmentVariableWithSecretFragmentNode } from '../types/EnvironmentVariableWithSecret';
|
||||
|
||||
type EnvironmentVariableWithLinkedEnvironments = EnvironmentVariableFragment & {
|
||||
linkedEnvironments?: EnvironmentVariableEnvironment[] | null;
|
||||
linkedEnvironments?: string[] | null;
|
||||
};
|
||||
|
||||
export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
|
||||
@@ -22,6 +21,31 @@ export type EnvironmentVariableWithFileContent = EnvironmentVariableFragment & {
|
||||
};
|
||||
|
||||
export const EnvironmentVariablesQuery = {
|
||||
async environmentVariableEnvironmentsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
appId: string
|
||||
): Promise<string[]> {
|
||||
const data = await withErrorHandlingAsync(
|
||||
graphqlClient
|
||||
.query(
|
||||
gql`
|
||||
query AppEnvironmentVariableEnvironments($appId: String!) {
|
||||
app {
|
||||
byId(appId: $appId) {
|
||||
id
|
||||
environmentVariableEnvironments
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ appId },
|
||||
{ additionalTypenames: ['App'] }
|
||||
)
|
||||
.toPromise()
|
||||
);
|
||||
|
||||
return data.app?.byId.environmentVariableEnvironments ?? [];
|
||||
},
|
||||
async byAppIdWithSensitiveAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
{
|
||||
@@ -31,7 +55,7 @@ export const EnvironmentVariablesQuery = {
|
||||
includeFileContent = false,
|
||||
}: {
|
||||
appId: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
filterNames?: string[];
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
@@ -80,7 +104,7 @@ export const EnvironmentVariablesQuery = {
|
||||
includeFileContent = false,
|
||||
}: {
|
||||
appId: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
filterNames?: string[];
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
@@ -128,7 +152,7 @@ export const EnvironmentVariablesQuery = {
|
||||
}: {
|
||||
appId: string;
|
||||
filterNames?: string[];
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
): Promise<(EnvironmentVariableWithFileContent & EnvironmentVariableWithLinkedEnvironments)[]> {
|
||||
@@ -178,7 +202,7 @@ export const EnvironmentVariablesQuery = {
|
||||
}: {
|
||||
appId: string;
|
||||
filterNames?: string[];
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
includeFileContent?: boolean;
|
||||
}
|
||||
): Promise<EnvironmentVariableWithFileContent[]> {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const BuildFragmentNode = gql`
|
||||
id
|
||||
displayName
|
||||
}
|
||||
logFiles
|
||||
project {
|
||||
__typename
|
||||
id
|
||||
|
||||
@@ -9,22 +9,26 @@ describe('installDependencies', () => {
|
||||
});
|
||||
|
||||
describe('promptForPackageManagerAsync', () => {
|
||||
it.each(['npm', 'yarn', 'pnpm'])('returns %s when user selects %s', async packageManager => {
|
||||
jest.mocked(selectAsync).mockResolvedValue(packageManager as PackageManager);
|
||||
it.each(['npm', 'yarn', 'pnpm', 'bun'])(
|
||||
'returns %s when user selects %s',
|
||||
async packageManager => {
|
||||
jest.mocked(selectAsync).mockResolvedValue(packageManager as PackageManager);
|
||||
|
||||
const result = await promptForPackageManagerAsync();
|
||||
expect(selectAsync).toHaveBeenCalledWith(
|
||||
'Which package manager would you like to use?',
|
||||
[
|
||||
{ title: 'npm', value: 'npm' },
|
||||
{ title: 'Yarn', value: 'yarn' },
|
||||
{ title: 'pnpm', value: 'pnpm' },
|
||||
],
|
||||
{ initial: 'npm' }
|
||||
);
|
||||
const result = await promptForPackageManagerAsync();
|
||||
expect(selectAsync).toHaveBeenCalledWith(
|
||||
'Which package manager would you like to use?',
|
||||
[
|
||||
{ title: 'bun', value: 'bun' },
|
||||
{ title: 'npm', value: 'npm' },
|
||||
{ title: 'pnpm', value: 'pnpm' },
|
||||
{ title: 'yarn', value: 'yarn' },
|
||||
],
|
||||
{ initial: 'npm' }
|
||||
);
|
||||
|
||||
expect(result).toBe(packageManager);
|
||||
});
|
||||
expect(result).toBe(packageManager);
|
||||
}
|
||||
);
|
||||
|
||||
it('defaults to npm as initial selection', async () => {
|
||||
jest.mocked(selectAsync).mockResolvedValue('npm');
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import { runCommandAsync } from './runCommand';
|
||||
import { selectAsync } from '../prompts';
|
||||
|
||||
export type PackageManager = 'npm' | 'yarn' | 'pnpm';
|
||||
export const PACKAGE_MANAGERS = ['bun', 'npm', 'pnpm', 'yarn'] as const;
|
||||
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
||||
|
||||
export async function promptForPackageManagerAsync(): Promise<PackageManager> {
|
||||
return await selectAsync(
|
||||
'Which package manager would you like to use?',
|
||||
[
|
||||
{ title: 'npm', value: 'npm' },
|
||||
{ title: 'Yarn', value: 'yarn' },
|
||||
{ title: 'pnpm', value: 'pnpm' },
|
||||
],
|
||||
(['bun', 'npm', 'pnpm', 'yarn'] as const).map(manager => ({ title: manager, value: manager })),
|
||||
{ initial: 'npm' }
|
||||
);
|
||||
}
|
||||
|
||||
export async function installDependenciesAsync({
|
||||
outputLevel = 'default',
|
||||
projectDir,
|
||||
packageManager = 'npm',
|
||||
}: {
|
||||
outputLevel?: 'default' | 'none';
|
||||
projectDir: string;
|
||||
packageManager?: PackageManager;
|
||||
}): Promise<void> {
|
||||
@@ -35,5 +34,6 @@ export async function installDependenciesAsync({
|
||||
!(line === packageManager)
|
||||
);
|
||||
},
|
||||
showOutput: outputLevel !== 'none',
|
||||
});
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export async function runCommandAsync({
|
||||
shouldShowStderrLine,
|
||||
shouldPrintStderrLineAsStdout,
|
||||
showSpinner = true,
|
||||
showOutput = true,
|
||||
}: {
|
||||
cwd?: string;
|
||||
args: string[];
|
||||
@@ -18,6 +19,7 @@ export async function runCommandAsync({
|
||||
shouldShowStderrLine?: (line: string) => boolean;
|
||||
shouldPrintStderrLineAsStdout?: (line: string) => boolean;
|
||||
showSpinner?: boolean;
|
||||
showOutput?: boolean;
|
||||
}): Promise<void> {
|
||||
Log.log(`🏗️ Running ${chalk.bold(`${command} ${args.join(' ')}`)}...`);
|
||||
let spinner: Ora | undefined;
|
||||
@@ -25,36 +27,39 @@ export async function runCommandAsync({
|
||||
spinner = ora(`${chalk.bold(`${command} ${args.join(' ')}`)}`).start();
|
||||
}
|
||||
const spawnPromise = spawnAsync(command, args, {
|
||||
stdio: ['inherit', 'pipe', 'pipe'],
|
||||
stdio: showOutput ? ['inherit', 'pipe', 'pipe'] : 'ignore',
|
||||
cwd,
|
||||
});
|
||||
const {
|
||||
child: { stdout, stderr },
|
||||
} = spawnPromise;
|
||||
if (!stdout || !stderr) {
|
||||
throw new Error(`Failed to spawn ${command}`);
|
||||
|
||||
if (showOutput) {
|
||||
const {
|
||||
child: { stdout, stderr },
|
||||
} = spawnPromise;
|
||||
if (!stdout || !stderr) {
|
||||
throw new Error(`Failed to spawn ${command}`);
|
||||
}
|
||||
|
||||
stdout.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
Log.log(`${chalk.gray(`[${command}]`)} ${line}`);
|
||||
}
|
||||
});
|
||||
stderr.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
if (shouldShowStderrLine && !shouldShowStderrLine(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const log = `${chalk.gray(`[${command}]`)} ${line}`;
|
||||
if (shouldPrintStderrLineAsStdout?.(line)) {
|
||||
Log.log(log);
|
||||
} else {
|
||||
Log.warn(`${chalk.gray(`[${command}]`)} ${line}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stdout.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
Log.log(`${chalk.gray(`[${command}]`)} ${line}`);
|
||||
}
|
||||
});
|
||||
stderr.on('data', data => {
|
||||
for (const line of data.toString().trim().split('\n')) {
|
||||
if (shouldShowStderrLine && !shouldShowStderrLine(line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const log = `${chalk.gray(`[${command}]`)} ${line}`;
|
||||
if (shouldPrintStderrLineAsStdout?.(line)) {
|
||||
Log.log(log);
|
||||
} else {
|
||||
Log.warn(`${chalk.gray(`[${command}]`)} ${line}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await spawnPromise;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,10 +1,31 @@
|
||||
import chalk from 'chalk';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
|
||||
import { DefaultEnvironment } from '../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EnvironmentSecretType, EnvironmentVariableVisibility } from '../graphql/generated';
|
||||
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
import { RequestedPlatform } from '../platform';
|
||||
import { promptAsync, selectAsync } from '../prompts';
|
||||
|
||||
const DEFAULT_ENVIRONMENTS = Object.values(DefaultEnvironment);
|
||||
|
||||
export async function getProjectEnvironmentVariableEnvironmentsAsync(
|
||||
graphqlClient: ExpoGraphqlClient,
|
||||
projectId: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
const environments = await EnvironmentVariablesQuery.environmentVariableEnvironmentsAsync(
|
||||
graphqlClient,
|
||||
projectId
|
||||
);
|
||||
return environments;
|
||||
} catch {
|
||||
throw new Error('Failed to fetch available environments');
|
||||
}
|
||||
}
|
||||
|
||||
const CUSTOM_ENVIRONMENT_VALUE = '~~CUSTOM~~';
|
||||
|
||||
export async function promptVariableTypeAsync(
|
||||
nonInteractive: boolean,
|
||||
initialType?: EnvironmentSecretType
|
||||
@@ -44,6 +65,24 @@ export function parseVisibility(
|
||||
}
|
||||
}
|
||||
|
||||
async function promptCustomEnvironmentAsync(): Promise<string> {
|
||||
const { customEnvironment } = await promptAsync({
|
||||
type: 'text',
|
||||
name: 'customEnvironment',
|
||||
message: 'Enter custom environment name:',
|
||||
validate: (value: string) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return 'Environment name cannot be empty';
|
||||
}
|
||||
if (!value.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||
return 'Environment name may only contain letters, numbers, underscores, and hyphens';
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
return customEnvironment;
|
||||
}
|
||||
|
||||
export async function promptVariableVisibilityAsync(
|
||||
nonInteractive: boolean,
|
||||
selectedVisibility?: EnvironmentVariableVisibility | null
|
||||
@@ -74,49 +113,93 @@ export async function promptVariableVisibilityAsync(
|
||||
|
||||
type EnvironmentPromptArgs = {
|
||||
nonInteractive: boolean;
|
||||
selectedEnvironments?: EnvironmentVariableEnvironment[];
|
||||
availableEnvironments?: EnvironmentVariableEnvironment[];
|
||||
selectedEnvironments?: string[];
|
||||
graphqlClient?: ExpoGraphqlClient;
|
||||
projectId?: string;
|
||||
canEnterCustomEnvironment?: boolean;
|
||||
};
|
||||
|
||||
export function promptVariableEnvironmentAsync(
|
||||
input: EnvironmentPromptArgs & { multiple: true }
|
||||
): Promise<EnvironmentVariableEnvironment[]>;
|
||||
): Promise<string[]>;
|
||||
export function promptVariableEnvironmentAsync(
|
||||
input: EnvironmentPromptArgs & { multiple?: false }
|
||||
): Promise<EnvironmentVariableEnvironment>;
|
||||
): Promise<string>;
|
||||
|
||||
export async function promptVariableEnvironmentAsync({
|
||||
nonInteractive,
|
||||
selectedEnvironments,
|
||||
multiple = false,
|
||||
availableEnvironments,
|
||||
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<
|
||||
EnvironmentVariableEnvironment[] | EnvironmentVariableEnvironment
|
||||
> {
|
||||
canEnterCustomEnvironment = false,
|
||||
graphqlClient,
|
||||
projectId,
|
||||
}: EnvironmentPromptArgs & { multiple?: boolean }): Promise<string[] | string> {
|
||||
if (nonInteractive) {
|
||||
throw new Error(
|
||||
'The `--environment` flag must be set when running in `--non-interactive` mode.'
|
||||
);
|
||||
}
|
||||
if (!multiple) {
|
||||
return await selectAsync(
|
||||
'Select environment:',
|
||||
(availableEnvironments ?? Object.values(EnvironmentVariableEnvironment)).map(environment => ({
|
||||
title: environment.toLocaleLowerCase(),
|
||||
value: environment,
|
||||
}))
|
||||
|
||||
let allEnvironments: string[] = DEFAULT_ENVIRONMENTS;
|
||||
if (graphqlClient && projectId) {
|
||||
const projectEnvironments = await getProjectEnvironmentVariableEnvironmentsAsync(
|
||||
graphqlClient,
|
||||
projectId
|
||||
);
|
||||
allEnvironments = [...new Set([...DEFAULT_ENVIRONMENTS, ...projectEnvironments])];
|
||||
}
|
||||
|
||||
if (!multiple) {
|
||||
const choices = allEnvironments.map(environment => ({
|
||||
title: environment,
|
||||
value: environment,
|
||||
}));
|
||||
|
||||
if (canEnterCustomEnvironment) {
|
||||
choices.push({
|
||||
title: 'Other (enter custom environment)',
|
||||
value: CUSTOM_ENVIRONMENT_VALUE,
|
||||
});
|
||||
}
|
||||
|
||||
const selectedEnvironment = await selectAsync('Select environment:', choices);
|
||||
|
||||
if (selectedEnvironment === CUSTOM_ENVIRONMENT_VALUE) {
|
||||
return await promptCustomEnvironmentAsync();
|
||||
}
|
||||
|
||||
return selectedEnvironment;
|
||||
}
|
||||
|
||||
const choices = allEnvironments.map(environment => ({
|
||||
title: environment,
|
||||
value: environment,
|
||||
selected: selectedEnvironments?.includes(environment),
|
||||
}));
|
||||
|
||||
if (canEnterCustomEnvironment) {
|
||||
choices.push({
|
||||
title: 'Other (enter custom environment)',
|
||||
value: CUSTOM_ENVIRONMENT_VALUE,
|
||||
selected: false,
|
||||
});
|
||||
}
|
||||
|
||||
const { environments } = await promptAsync({
|
||||
message: 'Select environment:',
|
||||
name: 'environments',
|
||||
type: 'multiselect',
|
||||
choices: Object.values(EnvironmentVariableEnvironment).map(environment => ({
|
||||
title: environment.toLocaleLowerCase(),
|
||||
value: environment,
|
||||
selected: selectedEnvironments?.includes(environment),
|
||||
})),
|
||||
choices,
|
||||
});
|
||||
|
||||
if (environments?.includes(CUSTOM_ENVIRONMENT_VALUE)) {
|
||||
const customEnvironment = await promptCustomEnvironmentAsync();
|
||||
const filteredEnvironments = environments.filter(
|
||||
(env: string) => env !== CUSTOM_ENVIRONMENT_VALUE
|
||||
);
|
||||
return [...filteredEnvironments, customEnvironment];
|
||||
}
|
||||
|
||||
return environments;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
import formatFields from './formatFields';
|
||||
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
|
||||
import {
|
||||
EnvironmentSecretType,
|
||||
EnvironmentVariableFragment,
|
||||
@@ -10,12 +9,6 @@ import {
|
||||
} from '../graphql/generated';
|
||||
import { EnvironmentVariableWithFileContent } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
|
||||
export function isEnvironment(environment: string): environment is EnvironmentVariableEnvironment {
|
||||
return Object.values(EnvironmentVariableEnvironment).includes(
|
||||
environment as EnvironmentVariableEnvironment
|
||||
);
|
||||
}
|
||||
|
||||
export function formatVariableName(variable: EnvironmentVariableFragment): string {
|
||||
const name = variable.name;
|
||||
const scope = variable.scope === EnvironmentVariableScope.Project ? 'project' : 'shared';
|
||||
@@ -53,8 +46,8 @@ export function formatVariableValue(variable: EnvironmentVariableWithFileContent
|
||||
}
|
||||
|
||||
export async function performForEnvironmentsAsync(
|
||||
environments: EnvironmentVariableEnvironment[] | null,
|
||||
fun: (environment: EnvironmentVariableEnvironment | undefined) => Promise<any>
|
||||
environments: string[] | null,
|
||||
fun: (environment: string | undefined) => Promise<any>
|
||||
): Promise<any[]> {
|
||||
const selectedEnvironments = environments ?? [undefined];
|
||||
return await Promise.all(selectedEnvironments.map(env => fun(env)));
|
||||
|
||||
@@ -8,7 +8,6 @@ import path from 'node:path';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
import { pack } from 'tar-stream';
|
||||
|
||||
import { EnvironmentVariableEnvironment } from '../build/utils/environment';
|
||||
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
|
||||
import { EnvironmentVariablesQuery } from '../graphql/queries/EnvironmentVariablesQuery';
|
||||
|
||||
@@ -191,7 +190,7 @@ export interface CreateManifestResult {
|
||||
interface CreateManifestParams {
|
||||
projectId: string;
|
||||
projectDir: string;
|
||||
environment?: EnvironmentVariableEnvironment;
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
/** Creates a manifest configuration sent up for deployment */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@expo/eas-json",
|
||||
"description": "A library for interacting with eas.json",
|
||||
"version": "16.20.1",
|
||||
"version": "16.23.0",
|
||||
"author": "Expo <[email protected]>",
|
||||
"bugs": "https://github.com/expo/eas-cli/issues",
|
||||
"dependencies": {
|
||||
|
||||
@@ -214,7 +214,8 @@
|
||||
"environment": {
|
||||
"description": "Environment to use for the server-side defined EAS environment variables during build process and command execution. Learn more: https://docs.expo.dev/eas/environment-variables/",
|
||||
"markdownDescription": "Environment to use for the server-side defined EAS environment variables during build process and command execution. [Learn more](https://docs.expo.dev/eas/environment-variables/)",
|
||||
"enum": ["production", "preview", "development"]
|
||||
"examples": ["production", "preview", "development"],
|
||||
"type": "string"
|
||||
},
|
||||
"android": {
|
||||
"$ref": "#/definitions/BuildProfileAndroid"
|
||||
|
||||
@@ -129,6 +129,35 @@ test('valid eas.json with specified environment', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('supports custom environment names', async () => {
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
build: {
|
||||
development: {
|
||||
environment: 'my-custom-environment',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const accessor = EasJsonAccessor.fromProjectPath('/project');
|
||||
const iosProfile = await EasJsonUtils.getBuildProfileAsync(accessor, Platform.IOS, 'development');
|
||||
const androidProfile = await EasJsonUtils.getBuildProfileAsync(
|
||||
accessor,
|
||||
Platform.ANDROID,
|
||||
'development'
|
||||
);
|
||||
expect(androidProfile).toEqual({
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
environment: 'my-custom-environment',
|
||||
});
|
||||
|
||||
expect(iosProfile).toEqual({
|
||||
credentialsSource: 'remote',
|
||||
distribution: 'store',
|
||||
environment: 'my-custom-environment',
|
||||
});
|
||||
});
|
||||
|
||||
test('valid eas.json with specified keystoreName', async () => {
|
||||
const keystoreName = 'this-is-a-keystore-name';
|
||||
await fs.writeJson('/project/eas.json', {
|
||||
|
||||
@@ -71,7 +71,15 @@ const CommonBuildProfileSchema = Joi.object({
|
||||
// credentials
|
||||
withoutCredentials: Joi.boolean(),
|
||||
|
||||
environment: Joi.string().valid('preview', 'production', 'development'),
|
||||
environment: Joi.string()
|
||||
.min(3)
|
||||
.max(100)
|
||||
.pattern(/^[a-z0-9_-]+$/)
|
||||
.messages({
|
||||
'string.min': 'Environment name must be at least 3 characters.',
|
||||
'string.max': 'Environment name must not be longer than 100 characters.',
|
||||
'string.pattern.base': 'Environment name can only contain lowercase letters, numbers, dashes and underscores.',
|
||||
}),
|
||||
});
|
||||
|
||||
const PlatformBuildProfileSchema = CommonBuildProfileSchema.concat(
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface CommonBuildProfile {
|
||||
// credentials
|
||||
withoutCredentials?: boolean;
|
||||
|
||||
environment?: 'preview' | 'production' | 'development';
|
||||
environment?: string;
|
||||
}
|
||||
|
||||
interface PlatformBuildProfile extends Omit<CommonBuildProfile, 'autoIncrement'> {
|
||||
|
||||
@@ -1549,6 +1549,16 @@
|
||||
semver "^7.6.2"
|
||||
zod "^4.1.3"
|
||||
|
||||
"@expo/[email protected]":
|
||||
version "1.0.239"
|
||||
resolved "https://registry.yarnpkg.com/@expo/eas-build-job/-/eas-build-job-1.0.239.tgz#025afd9c3fb9086cbb053ccbc08b7683aa74bbc4"
|
||||
integrity sha512-5P/dQL2S2iEyTvGKOPN95r7cNFW3HgnA4PNqXU0s5ctb10837sMX6/A5XgIg/Ow0SFGZRiP4cK9ZFAiK3m2Ong==
|
||||
dependencies:
|
||||
"@expo/logger" "1.0.221"
|
||||
joi "^17.13.1"
|
||||
semver "^7.6.2"
|
||||
zod "^4.1.3"
|
||||
|
||||
"@expo/env@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@expo/env/-/env-1.0.0.tgz#8e22894fae04ba6dc41fe0f1351a4973d410273a"
|
||||
@@ -5588,6 +5598,11 @@ chalk@^5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.3.0.tgz#67c20a7ebef70e7f3970a01f90fa210cb6860385"
|
||||
integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==
|
||||
|
||||
chalk@^5.6.2:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.6.2.tgz#b1238b6e23ea337af71c7f8a295db5af0c158aea"
|
||||
integrity sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==
|
||||
|
||||
[email protected]:
|
||||
version "1.0.15"
|
||||
resolved "https://registry.yarnpkg.com/change-case-all/-/change-case-all-1.0.15.tgz#de29393167fc101d646cd76b0ef23e27d09756ad"
|
||||
@@ -5688,6 +5703,13 @@ cli-cursor@^2.1.0:
|
||||
dependencies:
|
||||
restore-cursor "^2.0.0"
|
||||
|
||||
cli-cursor@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-5.0.0.tgz#24a4831ecf5a6b01ddeb32fb71a4b2088b0dce38"
|
||||
integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==
|
||||
dependencies:
|
||||
restore-cursor "^5.0.0"
|
||||
|
||||
[email protected], cli-progress@^3.10.0, cli-progress@^3.12.0:
|
||||
version "3.12.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942"
|
||||
@@ -5705,10 +5727,10 @@ cli-spinners@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41"
|
||||
integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
|
||||
|
||||
cli-spinners@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f"
|
||||
integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==
|
||||
cli-spinners@^3.2.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-3.3.0.tgz#2ba7c98b4f4662e67315b5634365661be8574440"
|
||||
integrity sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ==
|
||||
|
||||
cli-table@^0.3.1:
|
||||
version "0.3.11"
|
||||
@@ -7897,6 +7919,11 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||
|
||||
get-east-asian-width@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz#9bc4caa131702b4b61729cb7e42735bc550c9ee6"
|
||||
integrity sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==
|
||||
|
||||
get-intrinsic@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.0.1.tgz#94a9768fcbdd0595a1c9273aacf4c89d075631be"
|
||||
@@ -9095,6 +9122,11 @@ is-interactive@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e"
|
||||
integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==
|
||||
|
||||
is-interactive@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90"
|
||||
integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==
|
||||
|
||||
is-lambda@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
|
||||
@@ -9330,6 +9362,11 @@ is-unicode-supported@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||
|
||||
is-unicode-supported@^2.0.0, is-unicode-supported@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz#09f0ab0de6d3744d48d265ebb98f65d11f2a9b3a"
|
||||
integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
|
||||
|
||||
is-upper-case@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/is-upper-case/-/is-upper-case-2.0.2.tgz#f1105ced1fe4de906a5f39553e7d3803fd804649"
|
||||
@@ -10531,6 +10568,14 @@ log-symbols@^4.0.0:
|
||||
dependencies:
|
||||
chalk "^4.0.0"
|
||||
|
||||
log-symbols@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-7.0.1.tgz#f52e68037d96f589fc572ff2193dc424d48c195b"
|
||||
integrity sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==
|
||||
dependencies:
|
||||
is-unicode-supported "^2.0.0"
|
||||
yoctocolors "^2.1.1"
|
||||
|
||||
log-update@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
|
||||
@@ -10902,6 +10947,11 @@ mimic-fn@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mimic-function@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076"
|
||||
integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
|
||||
|
||||
mimic-response@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||
@@ -11901,6 +11951,13 @@ onetime@^5.1.0, onetime@^5.1.2:
|
||||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
onetime@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-7.0.0.tgz#9f16c92d8c9ef5120e3acd9dd9957cceecc1ab60"
|
||||
integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==
|
||||
dependencies:
|
||||
mimic-function "^5.0.0"
|
||||
|
||||
open@^8.0.4, open@^8.4.0:
|
||||
version "8.4.0"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-8.4.0.tgz#345321ae18f8138f82565a910fdc6b39e8c244f8"
|
||||
@@ -11922,20 +11979,6 @@ optionator@^0.9.3:
|
||||
prelude-ls "^1.2.1"
|
||||
type-check "^0.4.0"
|
||||
|
||||
[email protected]:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8"
|
||||
integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w==
|
||||
dependencies:
|
||||
chalk "^4.1.0"
|
||||
cli-cursor "^3.1.0"
|
||||
cli-spinners "^2.4.0"
|
||||
is-interactive "^1.0.0"
|
||||
log-symbols "^4.0.0"
|
||||
mute-stream "0.0.8"
|
||||
strip-ansi "^6.0.0"
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
[email protected]:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-5.3.0.tgz#fb832899d3a1372fe71c8b2c534bbfe74961bb6f"
|
||||
@@ -11950,6 +11993,21 @@ [email protected]:
|
||||
strip-ansi "^6.0.0"
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
[email protected]:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-9.0.0.tgz#945236f5ce78a024cf4c25df6c46ecd09ab6e685"
|
||||
integrity sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==
|
||||
dependencies:
|
||||
chalk "^5.6.2"
|
||||
cli-cursor "^5.0.0"
|
||||
cli-spinners "^3.2.0"
|
||||
is-interactive "^2.0.0"
|
||||
is-unicode-supported "^2.1.0"
|
||||
log-symbols "^7.0.1"
|
||||
stdin-discarder "^0.2.2"
|
||||
string-width "^8.1.0"
|
||||
strip-ansi "^7.1.2"
|
||||
|
||||
ora@^3.4.0:
|
||||
version "3.4.0"
|
||||
resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
|
||||
@@ -13100,6 +13158,14 @@ restore-cursor@^3.1.0:
|
||||
onetime "^5.1.0"
|
||||
signal-exit "^3.0.2"
|
||||
|
||||
restore-cursor@^5.0.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-5.1.0.tgz#0766d95699efacb14150993f55baf0953ea1ebe7"
|
||||
integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==
|
||||
dependencies:
|
||||
onetime "^7.0.0"
|
||||
signal-exit "^4.1.0"
|
||||
|
||||
[email protected]:
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
|
||||
@@ -13483,7 +13549,7 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||
|
||||
signal-exit@^4.0.1:
|
||||
signal-exit@^4.0.1, signal-exit@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
@@ -13779,6 +13845,11 @@ [email protected]:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
stdin-discarder@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz#390037f44c4ae1a1ae535c5fe38dc3aba8d997be"
|
||||
integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==
|
||||
|
||||
[email protected]:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/stream-buffers/-/stream-buffers-2.2.0.tgz#91d5f5130d1cef96dcfa7f726945188741d09ee4"
|
||||
@@ -13857,6 +13928,14 @@ string-width@^5.0.1, string-width@^5.1.2:
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
string-width@^8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-8.1.0.tgz#9e9fb305174947cf45c30529414b5da916e9e8d1"
|
||||
integrity sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==
|
||||
dependencies:
|
||||
get-east-asian-width "^1.3.0"
|
||||
strip-ansi "^7.1.0"
|
||||
|
||||
string.prototype.matchall@^4.0.11:
|
||||
version "4.0.11"
|
||||
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a"
|
||||
@@ -14021,6 +14100,13 @@ strip-ansi@^7.0.1:
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-ansi@^7.1.0, strip-ansi@^7.1.2:
|
||||
version "7.1.2"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba"
|
||||
integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==
|
||||
dependencies:
|
||||
ansi-regex "^6.0.1"
|
||||
|
||||
strip-bom-buf@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom-buf/-/strip-bom-buf-1.0.0.tgz#1cb45aaf57530f4caf86c7f75179d2c9a51dd572"
|
||||
@@ -15630,6 +15716,11 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
yoctocolors@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yoctocolors/-/yoctocolors-2.1.2.tgz#d795f54d173494e7d8db93150cec0ed7f678c83a"
|
||||
integrity sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==
|
||||
|
||||
zod@^4.1.3:
|
||||
version "4.1.8"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.8.tgz#54f024adc634dd185006a8415da2bdd495b9d92b"
|
||||
|
||||
Reference in new issue
Block a user