Compare commits

..
Author SHA1 Message Date
Quinlan JungandWill Schurman d2889571fe Update packages/eas-cli/src/project/projectUtils.ts
Co-authored-by: Will Schurman <[email protected]>
2023-06-30 16:57:29 -07:00
Quinlan Jung 224a5b288c update CHANGELOG.md 2023-06-30 23:35:11 +00:00
Quinlan Jung 16629e1ab5 remove draft function 2023-06-30 16:30:13 -07:00
Quinlan Jung d5a744b938 [eas-cli] remove useClassicUpdates upon configuration 2023-06-30 16:17:10 -07:00
Quinlan Jung 8f39b303c9 [eas-cli] bump config dependencies 2023-06-30 16:16:00 -07:00
211 changed files with 2605 additions and 14832 deletions

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Expo Developers Discord
url: https://chat.expo.dev/
about: Join other developers in discussions regarding Expo and EAS.
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
with:
node-version: 18
- name: Checkout Pull Request
run: gh pr checkout ${{ github.event.issue.number }}
run: hub pr checkout ${{ github.event.issue.number }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install dependencies
+5 -5
View File
@@ -29,7 +29,7 @@ jobs:
with:
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build-ci
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
@@ -70,7 +70,7 @@ jobs:
with:
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: yarn build-ci
- run: yarn build
- name: Build tarballs
working-directory: ${{ github.workspace }}/packages/eas-cli
run: |
@@ -99,7 +99,7 @@ jobs:
node-version: 18
- run: yarn install --frozen-lockfile --check-files
- run: sudo apt-get install nsis
- run: yarn build-ci
- run: yarn build
- run: |
yarn pretarball-ci
yarn oclif pack:win
@@ -142,7 +142,7 @@ jobs:
- name: Install dependencies
run: yarn install --frozen-lockfile --check-files
- name: Build
run: yarn build-ci
run: yarn build
- name: Publish packages to npm
run: yarn lerna publish from-package --yes --no-verify-access
env:
@@ -175,7 +175,7 @@ jobs:
working-directory: ./scripts
run: yarn --silent release-changelog $EAS_CLI_VERSION > /tmp/current-changelog
- name: Publish release
run: gh release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
run: hub release edit v$EAS_CLI_VERSION --draft=false -F /tmp/current-changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit and push
+3 -302
View File
@@ -8,315 +8,16 @@ This is the log of notable changes to EAS CLI and related packages.
### 🎉 New features
- Add `--profile` flag to `eas build:run` command. ([#2035](https://github.com/expo/eas-cli/pull/2035) by [@szdziedzic](https://github.com/szdziedzic))
### 🐛 Bug fixes
### 🧹 Chores
## [5.8.0](https://github.com/expo/eas-cli/releases/tag/v5.8.0) - 2023-11-13
### 🎉 New features
- Move channel:rollout out of developer preview. ([#2114](https://github.com/expo/eas-cli/pull/2114) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Fixed provisioning of new devices into an existing profile. ([#2119](https://github.com/expo/eas-cli/pull/2119) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🧹 Chores
- Update `@expo/package-manager` to `1.1.2` to change package manager resolution order. ([#2118](https://github.com/expo/eas-cli/pull/2118) by [@szdziedzic](https://github.com/szdziedzic))
## [5.7.0](https://github.com/expo/eas-cli/releases/tag/v5.7.0) - 2023-11-08
### 🎉 New features
- Add `EXPO_APPLE_TEAM_ID` and `EXPO_APPLE_PROVIDER_ID` support. ([#2091](https://github.com/expo/eas-cli/pull/2091) by [@EvanBacon](https://github.com/EvanBacon))
### 🧹 Chores
- Add link to SDK upgrade page for SDK-gated command error. ([#2106](https://github.com/expo/eas-cli/pull/2106) by [@wschurman](https://github.com/wschurman))
- Add `selectedImage` and `customNodeVersion` information to build metadata. ([#2113](https://github.com/expo/eas-cli/pull/2113) by [@szdziedzic](https://github.com/szdziedzic))
## [5.6.0](https://github.com/expo/eas-cli/releases/tag/v5.6.0) - 2023-10-27
### 🎉 New features
- Use corresponding submit profile when selecting build from EAS. ([#2101](https://github.com/expo/eas-cli/pull/2101) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🐛 Bug fixes
- Added `buildArtifactsUrl` to `eas-cli build:view --json` output. ([#2102](https://github.com/expo/eas-cli/pull/2102) by [@sjchmiela](https://github.com/sjchmiela))
## [5.5.0](https://github.com/expo/eas-cli/releases/tag/v5.5.0) - 2023-10-25
### 🎉 New features
- Add account type to the items in the prompt to select project owner. ([#2083](https://github.com/expo/eas-cli/pull/2083) by [@alanjhughes](https://github.com/alanjhughes))
- Gate roll back to embedded to expo-updates >= 0.19.0. ([#2094](https://github.com/expo/eas-cli/pull/2094) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- EAS Update: Increase asset upload timeout to 90s and reset on upload retry for slow connections. ([#2085](https://github.com/expo/eas-cli/pull/2085) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Add `requiredPackageManager` to metadata. ([#2067](https://github.com/expo/eas-cli/pull/2067) by [@kadikraman](https://github.com/kadikraman))
- Move `getVcsClient` into command context. ([#2086](https://github.com/expo/eas-cli/pull/2086) by [@Josh-McFarlin](https://github.com/Josh-McFarlin))
- Display Apple device creation date when listing devices. ([#2092](https://github.com/expo/eas-cli/pull/2092) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Clean up Intel resource classes code after their deletion. ([#2093](https://github.com/expo/eas-cli/pull/2093) by [@szdziedzic](https://github.com/szdziedzic))
- Update images descriptions in `eas.schema.json` and add info about the new JDK 17 image. ([#2099](https://github.com/expo/eas-cli/pull/2099) by [@szdziedzic](https://github.com/szdziedzic))
## [5.4.0](https://github.com/expo/eas-cli/releases/tag/v5.4.0) - 2023-09-28
### 🎉 New features
- Add support for the Tap to Pay on iPhone iOS entitlement. ([#2069](https://github.com/expo/eas-cli/pull/2069) by [@fobos531](https://github.com/fobos531))
## [5.3.1](https://github.com/expo/eas-cli/releases/tag/v5.3.1) - 2023-09-28
### 🧹 Chores
- Update EAS Build images description in our VSCode plugin. Add new `macos-ventura-13.6-xcode-15.0` image. ([#2068](https://github.com/expo/eas-cli/pull/2068) by [@szdziedzic](https://github.com/szdziedzic))
## [5.3.0](https://github.com/expo/eas-cli/releases/tag/v5.3.0) - 2023-09-25
### 🎉 New features
- Update: expose expo cli `--dev` flag as an argument. ([#2050](https://github.com/expo/eas-cli/pull/2050) by [@nderscore](https://github.com/nderscore))
- Support `bun` option in eas.json. ([#2055](https://github.com/expo/eas-cli/pull/2055) by [@kadikraman](https://github.com/kadikraman))
### 🐛 Bug fixes
- Support node aliases in .nvmrc. ([#2052](https://github.com/expo/eas-cli/pull/2052) by [@khamilowicz](https://github.com/khamilowicz))
### 🧹 Chores
- Rollouts: more robust printing function. ([#2047](https://github.com/expo/eas-cli/pull/2047) by [@quinlanj](https://github.com/quinlanj))
## [5.2.0](https://github.com/expo/eas-cli/releases/tag/v5.2.0) - 2023-09-05
### 🎉 New features
- Rollouts: json output for ci. ([#2037](https://github.com/expo/eas-cli/pull/2037) by [@quinlanj](https://github.com/quinlanj))
### 🐛 Bug fixes
- Update: only print channel-branch pairing if we created a channel. ([#2036](https://github.com/expo/eas-cli/pull/2036) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Added rollout tests. ([#2042](https://github.com/expo/eas-cli/pull/2042) by [@quinlanj](https://github.com/quinlanj))
- Remove unreachable codesigning option. ([#2041](https://github.com/expo/eas-cli/pull/2041) by [@quinlanj](https://github.com/quinlanj))
- Fix generated graphql tsc errors. ([#2039](https://github.com/expo/eas-cli/pull/2039) by [@quinlanj](https://github.com/quinlanj))
- Rollouts: view action for CI. ([#2040](https://github.com/expo/eas-cli/pull/2040) by [@quinlanj](https://github.com/quinlanj))
## [5.1.0](https://github.com/expo/eas-cli/releases/tag/v5.1.0) - 2023-09-01
### 🎉 New features
- Support `pnpm` option in eas.json. ([#1988](https://github.com/expo/eas-cli/pull/1988) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- Make app config error not repeat indefinitely. ([#2020](https://github.com/expo/eas-cli/pull/2020) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
## [5.0.2](https://github.com/expo/eas-cli/releases/tag/v5.0.2) - 2023-08-29
### 🐛 Bug fixes
- Add proper expo cli `--platform` flag handling when exporting updates. ([#1939](https://github.com/expo/eas-cli/pull/1939) by [@byCedric](https://github.com/byCedric))
## [5.0.1](https://github.com/expo/eas-cli/releases/tag/v5.0.1) - 2023-08-28
### 🐛 Bug fixes
- Pass `platform` argument to expo-cli correctly when using the `eas update` command. ([#2028](https://github.com/expo/eas-cli/pull/2028) by [@szdziedzic](https://github.com/szdziedzic))
## [5.0.0](https://github.com/expo/eas-cli/releases/tag/v5.0.0) - 2023-08-28
### 🛠 Breaking changes
- Only export at most ios and android dist for EAS updates. ([#2002](https://github.com/expo/eas-cli/pull/2002) by [@wschurman](https://github.com/wschurman))
### 🎉 New features
- Add rollback disambiguation command. ([#2004](https://github.com/expo/eas-cli/pull/2004) by [@wschurman](https://github.com/wschurman))
- Detect devices that fail to be provisioned, list them to the user and show the explanation message with the link to the devices page to check actual status. ([#2011](https://github.com/expo/eas-cli/pull/2011) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
- Add info to EAS Update asset upload process about asset counts and limits. ([#2013](https://github.com/expo/eas-cli/pull/2013) by [@wschurman](https://github.com/wschurman))
- .nvmrc support for setting node version. ([#1954](https://github.com/expo/eas-cli/pull/1954) by [@khamilowicz](https://github.com/khamilowicz))
### 🐛 Bug fixes
- Support republishing roll back to embedded updates. ([#2006](https://github.com/expo/eas-cli/pull/2006) by [@wschurman](https://github.com/wschurman))
- Configure updates as well when somebody tries to run a build with channel set. ([#2016](https://github.com/expo/eas-cli/pull/2016) by [@wschurman](https://github.com/wschurman))
- Fix printing bug: branch with no update. ([#2023](https://github.com/expo/eas-cli/pull/2023) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- More branch map utility functions. ([#2001](https://github.com/expo/eas-cli/pull/2001) by [@quinlanj](https://github.com/quinlanj))
- More branch mapping utils. ([#2003](https://github.com/expo/eas-cli/pull/2003) by [@quinlanj](https://github.com/quinlanj))
- Create a linked channel on build if not exists. ([#2017](https://github.com/expo/eas-cli/pull/2017) by [@quinlanj](https://github.com/quinlanj))
- Add `developmentClient` to metadata. ([#2015](https://github.com/expo/eas-cli/pull/2015) by [@szdziedzic](https://github.com/szdziedzic))
## [4.1.2](https://github.com/expo/eas-cli/releases/tag/v4.1.2) - 2023-08-10
### 🧹 Chores
- Make branch mapping utility files have no dependencies. ([#1993](https://github.com/expo/eas-cli/pull/1993) by [@quinlanj](https://github.com/quinlanj))
## [4.1.1](https://github.com/expo/eas-cli/releases/tag/v4.1.1) - 2023-08-08
### 🧹 Chores
- Logger to say website support is coming soon for rollouts. ([#1997](https://github.com/expo/eas-cli/pull/1997) by [@quinlanj](https://github.com/quinlanj))
## [4.1.0](https://github.com/expo/eas-cli/releases/tag/v4.1.0) - 2023-08-08
### 🎉 New features
- Pass credentials to custom iOS builds. ([#1989](https://github.com/expo/eas-cli/pull/1989) by [@szdziedzic](https://github.com/szdziedzic))
- Add the `withoutCredentials` option as a common build profile field in `eas.json`. ([#1994](https://github.com/expo/eas-cli/pull/1994) by [@szdziedzic](https://github.com/szdziedzic))
## [4.0.1](https://github.com/expo/eas-cli/releases/tag/v4.0.1) - 2023-08-08
### 🐛 Bug fixes
- Fix `eas channel:*` to work with empty branch mappings. ([#1992](https://github.com/expo/eas-cli/pull/1992) by [@quinlanj](https://github.com/quinlanj))
## [4.0.0](https://github.com/expo/eas-cli/releases/tag/v4.0.0) - 2023-08-07
### 🛠 Breaking changes
- Release redesigned `eas channel:rollout` into developer preview. The set of flag arguments are different, in addition to the workflow. ([#1986](https://github.com/expo/eas-cli/pull/1986) by [@quinlanj](https://github.com/quinlanj))
### 🎉 New features
- Option to add current Apple Silicon device without the need to manually provide the provisioning UDID. ([#1943](https://github.com/expo/eas-cli/pull/1943) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🐛 Bug fixes
- Fix rollout-preview ending with republish with code signing. ([#1978](https://github.com/expo/eas-cli/pull/1978) by [@wschurman](https://github.com/wschurman))
- Rollouts: fix fp precision. ([#1985](https://github.com/expo/eas-cli/pull/1985) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Fixing more grammar errors. ([#1980](https://github.com/expo/eas-cli/pull/1980) by [@quinlanj](https://github.com/quinlanj))
- Handle rollout edit edge case. ([#1981](https://github.com/expo/eas-cli/pull/1981) by [@quinlanj](https://github.com/quinlanj))
- Linked expo.fyi rollout article. ([#1991](https://github.com/expo/eas-cli/pull/1991) by [@quinlanj](https://github.com/quinlanj))
- Rollout ux improvements. ([#1984](https://github.com/expo/eas-cli/pull/1984) by [@quinlanj](https://github.com/quinlanj))
## [3.18.3](https://github.com/expo/eas-cli/releases/tag/v3.18.3) - 2023-08-03
### 🐛 Bug fixes
- Fix republishing with code signing. ([#1973](https://github.com/expo/eas-cli/pull/1973) by [@wschurman](https://github.com/wschurman))
### 🧹 Chores
- Add relationships flag to rollouts-preview. ([#1972](https://github.com/expo/eas-cli/pull/1972) by [@quinlanj](https://github.com/quinlanj))
- Get channel:{view,list,edit} to play nice with rollouts. ([#1974](https://github.com/expo/eas-cli/pull/1974) by [@quinlanj](https://github.com/quinlanj))
- Use 'roll out' instead of rollout for verbs. ([#1979](https://github.com/expo/eas-cli/pull/1979) by [@quinlanj](https://github.com/quinlanj))
## [3.18.2](https://github.com/expo/eas-cli/releases/tag/v3.18.2) - 2023-08-03
### 🐛 Bug fixes
- Revert adding `.nvmrc` support for setting node version. ([#1976](https://github.com/expo/eas-cli/pull/1976) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Use just a comma instead of `, ` when concatenating `keywords` in `eas metadata`. ([#1967](https://github.com/expo/eas-cli/pull/1967) by [@szdziedzic](https://github.com/szdziedzic))
## [3.18.1](https://github.com/expo/eas-cli/releases/tag/v3.18.1) - 2023-08-03
### 🐛 Bug fixes
- Pass correct group into `update:republish`. ([#1971](https://github.com/expo/eas-cli/pull/1971) by [@quinlanj](https://github.com/quinlanj))
### 🧹 Chores
- Make new rollouts version available for internal dogfooding. ([#1966](https://github.com/expo/eas-cli/pull/1966) by [@quinlanj](https://github.com/quinlanj))
- Change default runtime version policy for EAS Update to appVersion. ([#1968](https://github.com/expo/eas-cli/pull/1968) by [@quinlanj](https://github.com/quinlanj))
## [3.18.0](https://github.com/expo/eas-cli/releases/tag/v3.18.0) - 2023-08-02
### 🎉 New features
- .nvmrc support for setting node version. ([#1954](https://github.com/expo/eas-cli/pull/1954) by [@khamilowicz](https://github.com/khamilowicz))
- Provide credentials for custom Android builds. ([#1969](https://github.com/expo/eas-cli/pull/1969) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- More branch mapping utility. ([#1957](https://github.com/expo/eas-cli/pull/1957) by [@quinlanj](https://github.com/quinlanj))
- Utility classes to select existing rollouts and channels. ([#1958](https://github.com/expo/eas-cli/pull/1958) by [@quinlanj](https://github.com/quinlanj))
## [3.17.1](https://github.com/expo/eas-cli/releases/tag/v3.17.1) - 2023-07-27
### 🧹 Chores
- Unify channel graphql query types. ([#1949](https://github.com/expo/eas-cli/pull/1949) by [@quinlanj](https://github.com/quinlanj))
- Revert UpdateBranchWithCurrentGroupFragment. ([#1952](https://github.com/expo/eas-cli/pull/1952) by [@quinlanj](https://github.com/quinlanj))
- Fetch entire relay compliant dataset. ([#1953](https://github.com/expo/eas-cli/pull/1953) by [@quinlanj](https://github.com/quinlanj))
## [3.17.0](https://github.com/expo/eas-cli/releases/tag/v3.17.0) - 2023-07-24
### 🎉 New features
- Add `rollout` option for configuring Android submissions. ([#1938](https://github.com/expo/eas-cli/pull/1938) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Added branch mapping utility functions. ([#1944](https://github.com/expo/eas-cli/pull/1944) by [@quinlanj](https://github.com/quinlanj))
- Amend branch mapping utility functions. ([#1945](https://github.com/expo/eas-cli/pull/1945) by [@quinlanj](https://github.com/quinlanj))
- Handle error thrown when `intel-medium` resource class is not available as server-side defined error. ([#1947](https://github.com/expo/eas-cli/pull/1947) by [@szdziedzic](https://github.com/szdziedzic))
- Remove `intel-medium` from `eas.schema.json`, so it's not suggested as a valid value by our VSCode plugin. ([#1947](https://github.com/expo/eas-cli/pull/1947) by [@szdziedzic](https://github.com/szdziedzic))
## [3.16.0](https://github.com/expo/eas-cli/releases/tag/v3.16.0) - 2023-07-18
### 🎉 New features
- Add styling to SSO auth redirect completion page. ([#1929](https://github.com/expo/eas-cli/pull/1929) by [@wschurman](https://github.com/wschurman))
- Ignore entitlements from native template when `/ios` is gitignored. ([#1906](https://github.com/expo/eas-cli/pull/1906) by [@byCedric](https://github.com/byCedric))
- Use node server default port selection for SSO login server. ([#1930](https://github.com/expo/eas-cli/pull/1930) by [@wschurman](https://github.com/wschurman))
### 🐛 Bug fixes
- Fix incorrect handling of valid `inProgress` Android submission release status. ([#1934](https://github.com/expo/eas-cli/pull/1934) by [@szdziedzic](https://github.com/szdziedzic))
### 🧹 Chores
- Print friendly error msg in case account doesn't have required permission. ([#1867](https://github.com/expo/eas-cli/pull/1867) by [@firasrg](https://github.com/firasrg))
- Limit project file upload size to 2GB. ([#1928](https://github.com/expo/eas-cli/pull/1928) by [@khamilowicz](https://github.com/khamilowicz))
- Bump urql graphql client major version. ([#1936](https://github.com/expo/eas-cli/pull/1936) by [@quinlanj](https://github.com/quinlanj))
## [3.15.1](https://github.com/expo/eas-cli/releases/tag/v3.15.1) - 2023-07-11
### 🐛 Bug fixes
- Ensure useClassicUpdates is not set when using EAS Update commands. ([#1915](https://github.com/expo/eas-cli/pull/1915) by [@ide](https://github.com/ide))
- Handle multiple GraphQLErrors when receiving a CombinedError. ([#1924](https://github.com/expo/eas-cli/pull/1924) by [@radoslawkrzemien](https://github.com/radoslawkrzemien))
### 🧹 Chores
- Bump Expo package dependencies. ([#1911](https://github.com/expo/eas-cli/pull/1911) by [@brentvatne](https://github.com/brentvatne))
- Better bare workflow runtimeVersion error. ([#1910](https://github.com/expo/eas-cli/pull/1910) by [@quinlanj](https://github.com/quinlanj))
- Fix runtime version print logs. ([#1925](https://github.com/expo/eas-cli/pull/1925) by [@quinlanj](https://github.com/quinlanj))
- Add info about new `macos-ventura-13.4-xcode-14.3.1` image to `eas.schema.json`. ([#1920](https://github.com/expo/eas-cli/pull/1920) by [@szdziedzic](https://github.com/szdziedzic))
## [3.15.0](https://github.com/expo/eas-cli/releases/tag/v3.15.0) - 2023-06-30
### 🎉 New features
- Show build profile when selecting a build for eas build:run. ([#1901](https://github.com/expo/eas-cli/pull/1901) by [@keith-kurak](https://github.com/keith-kurak))
- Adds -m alias to --message in update/republish and removed README comment. ([#1905](https://github.com/expo/eas-cli/pull/1905) by [@pusongqi](https://github.com/pusongqi))
### 🐛 Bug fixes
### 🧹 Chores
- Better runtimeVersion output. ([#1894](https://github.com/expo/eas-cli/pull/1894) by [@quinlanj](https://github.com/quinlanj))
- Print better error message when uploading project archive tarball fails. ([#1897](https://github.com/expo/eas-cli/pull/1897) by [@szdziedzic](https://github.com/szdziedzic))
- Adds support for 2 new server-side errors related to build limits. ([#1921](https://github.com/expo/eas-cli/pull/1921) by [@sundeeppeswani](https://github.com/sundeeppeswani))
- Set useClassicUpdates to false on `eas update` and `eas update:configure`. ([#1914](https://github.com/expo/eas-cli/pull/1914) by [@quinlanj](https://github.com/quinlanj))
## [3.14.0](https://github.com/expo/eas-cli/releases/tag/v3.14.0) - 2023-06-20
-5
View File
@@ -1,5 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Instead of opening a GitHub issue for security vulnerabilities, send an email to our secure inbox: secure@expo.io.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"npmClient": "yarn",
"useWorkspaces": true,
"version": "5.9.0",
"version": "3.14.0",
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
}
-1
View File
@@ -6,7 +6,6 @@
],
"scripts": {
"build": "lerna run build",
"build-ci": "lerna run build-ci",
"build:tarballs:linux": "yarn build-for-size-check",
"build-for-size-check": "yarn build && yarn workspace eas-cli pretarball-ci && CLI_SIZE_CHECK=1 yarn workspace eas-cli oclif pack:tarballs --no-xz --targets linux-x64",
"typecheck": "lerna run typecheck",
+136 -196
View File
@@ -55,70 +55,69 @@ eas --help COMMAND
# Commands
<!-- commands -->
* [`eas account:login`](#eas-accountlogin)
* [`eas account:logout`](#eas-accountlogout)
* [`eas account:view`](#eas-accountview)
* [`eas analytics [STATUS]`](#eas-analytics-status)
* [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
* [`eas branch:create [NAME]`](#eas-branchcreate-name)
* [`eas branch:delete [NAME]`](#eas-branchdelete-name)
* [`eas branch:list`](#eas-branchlist)
* [`eas branch:rename`](#eas-branchrename)
* [`eas branch:view [NAME]`](#eas-branchview-name)
* [`eas build`](#eas-build)
* [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
* [`eas build:configure`](#eas-buildconfigure)
* [`eas build:inspect`](#eas-buildinspect)
* [`eas build:list`](#eas-buildlist)
* [`eas build:resign`](#eas-buildresign)
* [`eas build:run`](#eas-buildrun)
* [`eas build:submit`](#eas-buildsubmit)
* [`eas build:version:get`](#eas-buildversionget)
* [`eas build:version:set`](#eas-buildversionset)
* [`eas build:version:sync`](#eas-buildversionsync)
* [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
* [`eas channel:create [NAME]`](#eas-channelcreate-name)
* [`eas channel:edit [NAME]`](#eas-channeledit-name)
* [`eas channel:list`](#eas-channellist)
* [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
* [`eas channel:view [NAME]`](#eas-channelview-name)
* [`eas config`](#eas-config)
* [`eas credentials`](#eas-credentials)
* [`eas device:create`](#eas-devicecreate)
* [`eas device:delete`](#eas-devicedelete)
* [`eas device:list`](#eas-devicelist)
* [`eas device:rename`](#eas-devicerename)
* [`eas device:view [UDID]`](#eas-deviceview-udid)
* [`eas diagnostics`](#eas-diagnostics)
* [`eas help [COMMAND]`](#eas-help-command)
* [`eas init`](#eas-init)
* [`eas login`](#eas-login)
* [`eas logout`](#eas-logout)
* [`eas metadata:lint`](#eas-metadatalint)
* [`eas metadata:pull`](#eas-metadatapull)
* [`eas metadata:push`](#eas-metadatapush)
* [`eas open`](#eas-open)
* [`eas project:info`](#eas-projectinfo)
* [`eas project:init`](#eas-projectinit)
* [`eas secret:create`](#eas-secretcreate)
* [`eas secret:delete`](#eas-secretdelete)
* [`eas secret:list`](#eas-secretlist)
* [`eas secret:push`](#eas-secretpush)
* [`eas submit`](#eas-submit)
* [`eas update`](#eas-update)
* [`eas update:configure`](#eas-updateconfigure)
* [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
* [`eas update:list`](#eas-updatelist)
* [`eas update:republish`](#eas-updaterepublish)
* [`eas update:roll-back-to-embedded`](#eas-updateroll-back-to-embedded)
* [`eas update:rollback`](#eas-updaterollback)
* [`eas update:view GROUPID`](#eas-updateview-groupid)
* [`eas webhook:create`](#eas-webhookcreate)
* [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
* [`eas webhook:list`](#eas-webhooklist)
* [`eas webhook:update`](#eas-webhookupdate)
* [`eas webhook:view ID`](#eas-webhookview-id)
* [`eas whoami`](#eas-whoami)
- [`eas account:login`](#eas-accountlogin)
- [`eas account:logout`](#eas-accountlogout)
- [`eas account:view`](#eas-accountview)
- [`eas analytics [STATUS]`](#eas-analytics-status)
- [`eas autocomplete [SHELL]`](#eas-autocomplete-shell)
- [`eas branch:create [NAME]`](#eas-branchcreate-name)
- [`eas branch:delete [NAME]`](#eas-branchdelete-name)
- [`eas branch:list`](#eas-branchlist)
- [`eas branch:rename`](#eas-branchrename)
- [`eas branch:view [NAME]`](#eas-branchview-name)
- [`eas build`](#eas-build)
- [`eas build:cancel [BUILD_ID]`](#eas-buildcancel-build_id)
- [`eas build:configure`](#eas-buildconfigure)
- [`eas build:inspect`](#eas-buildinspect)
- [`eas build:list`](#eas-buildlist)
- [`eas build:resign`](#eas-buildresign)
- [`eas build:run`](#eas-buildrun)
- [`eas build:submit`](#eas-buildsubmit)
- [`eas build:version:get`](#eas-buildversionget)
- [`eas build:version:set`](#eas-buildversionset)
- [`eas build:version:sync`](#eas-buildversionsync)
- [`eas build:view [BUILD_ID]`](#eas-buildview-build_id)
- [`eas channel:create [NAME]`](#eas-channelcreate-name)
- [`eas channel:edit [NAME]`](#eas-channeledit-name)
- [`eas channel:list`](#eas-channellist)
- [`eas channel:rollout [CHANNEL]`](#eas-channelrollout-channel)
- [`eas channel:view [NAME]`](#eas-channelview-name)
- [`eas config`](#eas-config)
- [`eas credentials`](#eas-credentials)
- [`eas device:create`](#eas-devicecreate)
- [`eas device:delete`](#eas-devicedelete)
- [`eas device:list`](#eas-devicelist)
- [`eas device:rename`](#eas-devicerename)
- [`eas device:view [UDID]`](#eas-deviceview-udid)
- [`eas diagnostics`](#eas-diagnostics)
- [`eas help [COMMAND]`](#eas-help-command)
- [`eas init`](#eas-init)
- [`eas login`](#eas-login)
- [`eas logout`](#eas-logout)
- [`eas metadata:lint`](#eas-metadatalint)
- [`eas metadata:pull`](#eas-metadatapull)
- [`eas metadata:push`](#eas-metadatapush)
- [`eas open`](#eas-open)
- [`eas project:info`](#eas-projectinfo)
- [`eas project:init`](#eas-projectinit)
- [`eas secret:create`](#eas-secretcreate)
- [`eas secret:delete`](#eas-secretdelete)
- [`eas secret:list`](#eas-secretlist)
- [`eas secret:push`](#eas-secretpush)
- [`eas submit`](#eas-submit)
- [`eas update`](#eas-update)
- [`eas update:configure`](#eas-updateconfigure)
- [`eas update:delete GROUPID`](#eas-updatedelete-groupid)
- [`eas update:list`](#eas-updatelist)
- [`eas update:republish`](#eas-updaterepublish)
- [`eas update:view GROUPID`](#eas-updateview-groupid)
- [`eas webhook:create`](#eas-webhookcreate)
- [`eas webhook:delete [ID]`](#eas-webhookdelete-id)
- [`eas webhook:list`](#eas-webhooklist)
- [`eas webhook:update`](#eas-webhookupdate)
- [`eas webhook:view ID`](#eas-webhookview-id)
- [`eas whoami`](#eas-whoami)
## `eas account:login`
@@ -135,7 +134,7 @@ ALIASES
$ eas login
```
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/account/login.ts)_
_See code: [src/commands/account/login.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/account/login.ts)_
## `eas account:logout`
@@ -152,7 +151,7 @@ ALIASES
$ eas logout
```
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/account/logout.ts)_
_See code: [src/commands/account/logout.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/account/logout.ts)_
## `eas account:view`
@@ -169,7 +168,7 @@ ALIASES
$ eas whoami
```
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/account/view.ts)_
_See code: [src/commands/account/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/account/view.ts)_
## `eas analytics [STATUS]`
@@ -183,7 +182,7 @@ DESCRIPTION
display or change analytics settings
```
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/analytics.ts)_
_See code: [src/commands/analytics.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/analytics.ts)_
## `eas autocomplete [SHELL]`
@@ -233,7 +232,7 @@ DESCRIPTION
create a branch
```
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/branch/create.ts)_
_See code: [src/commands/branch/create.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/branch/create.ts)_
## `eas branch:delete [NAME]`
@@ -254,7 +253,7 @@ DESCRIPTION
delete a branch
```
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/branch/delete.ts)_
_See code: [src/commands/branch/delete.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/branch/delete.ts)_
## `eas branch:list`
@@ -274,7 +273,7 @@ DESCRIPTION
list all branches
```
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/branch/list.ts)_
_See code: [src/commands/branch/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/branch/list.ts)_
## `eas branch:rename`
@@ -294,7 +293,7 @@ DESCRIPTION
rename a branch
```
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/branch/rename.ts)_
_See code: [src/commands/branch/rename.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/branch/rename.ts)_
## `eas branch:view [NAME]`
@@ -317,7 +316,7 @@ DESCRIPTION
view a branch
```
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/branch/view.ts)_
_See code: [src/commands/branch/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/branch/view.ts)_
## `eas build`
@@ -347,7 +346,7 @@ DESCRIPTION
start a build
```
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/index.ts)_
_See code: [src/commands/build/index.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/index.ts)_
## `eas build:cancel [BUILD_ID]`
@@ -364,7 +363,7 @@ DESCRIPTION
cancel a build
```
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/cancel.ts)_
_See code: [src/commands/build/cancel.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/cancel.ts)_
## `eas build:configure`
@@ -381,7 +380,7 @@ DESCRIPTION
configure the project to support EAS Build
```
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/configure.ts)_
_See code: [src/commands/build/configure.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/configure.ts)_
## `eas build:inspect`
@@ -416,7 +415,7 @@ DESCRIPTION
inspect the state of the project at specific build stages, useful for troubleshooting
```
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/inspect.ts)_
_See code: [src/commands/build/inspect.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/inspect.ts)_
## `eas build:list`
@@ -454,7 +453,7 @@ DESCRIPTION
list all builds for your project
```
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/list.ts)_
_See code: [src/commands/build/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/list.ts)_
## `eas build:resign`
@@ -480,7 +479,7 @@ DESCRIPTION
re-sign a build archive
```
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/resign.ts)_
_See code: [src/commands/build/resign.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/resign.ts)_
## `eas build:run`
@@ -488,12 +487,10 @@ run simulator/emulator builds from eas-cli
```
USAGE
$ eas build:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios] [-e <value>]
[--offset <value>] [--limit <value>]
$ eas build:run [--latest | --id <value> | --path <value> | --url <value>] [-p android|ios] [--offset
<value>] [--limit <value>]
FLAGS
-e, --profile=PROFILE_NAME Name of the build profile used to create the build to run. When specified, only builds
created with the specified build profile will be queried.
-p, --platform=(android|ios)
--id=<value> ID of the simulator/emulator build to run
--latest Run the latest simulator/emulator build for specified platform
@@ -506,7 +503,7 @@ DESCRIPTION
run simulator/emulator builds from eas-cli
```
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/run.ts)_
_See code: [src/commands/build/run.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/run.ts)_
## `eas build:submit`
@@ -555,7 +552,7 @@ DESCRIPTION
get the latest version from EAS servers
```
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/version/get.ts)_
_See code: [src/commands/build/version/get.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/version/get.ts)_
## `eas build:version:set`
@@ -574,7 +571,7 @@ DESCRIPTION
update version of an app
```
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/version/set.ts)_
_See code: [src/commands/build/version/set.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/version/set.ts)_
## `eas build:version:sync`
@@ -593,7 +590,7 @@ DESCRIPTION
update a version in native code with a value stored on EAS servers
```
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/version/sync.ts)_
_See code: [src/commands/build/version/sync.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/version/sync.ts)_
## `eas build:view [BUILD_ID]`
@@ -610,7 +607,7 @@ DESCRIPTION
view a build for your project
```
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/build/view.ts)_
_See code: [src/commands/build/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/build/view.ts)_
## `eas channel:create [NAME]`
@@ -631,7 +628,7 @@ DESCRIPTION
create a channel
```
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/channel/create.ts)_
_See code: [src/commands/channel/create.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/channel/create.ts)_
## `eas channel:edit [NAME]`
@@ -653,7 +650,7 @@ DESCRIPTION
point a channel at a new branch
```
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/channel/edit.ts)_
_See code: [src/commands/channel/edit.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/channel/edit.ts)_
## `eas channel:list`
@@ -673,7 +670,7 @@ DESCRIPTION
list all channels
```
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/channel/list.ts)_
_See code: [src/commands/channel/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/channel/list.ts)_
## `eas channel:rollout [CHANNEL]`
@@ -681,31 +678,23 @@ Roll a new branch out on a channel incrementally.
```
USAGE
$ eas channel:rollout [CHANNEL] [--action create|edit|end|view] [--percent <value>] [--outcome
republish-and-revert|revert] [--branch <value>] [--runtime-version <value>] [--private-key-path <value>] [--json
--non-interactive]
$ eas channel:rollout [CHANNEL] [--branch <value>] [--percent <value>] [--end] [--json --non-interactive]
ARGUMENTS
CHANNEL channel on which the rollout should be done
FLAGS
--action=(create|edit|end|view) Rollout action to perform
--branch=<value> Branch to roll out. Use with --action=create
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--outcome=(republish-and-revert|revert) End outcome of rollout. Use with --action=end
--percent=<value> Percent of users to send to the new branch. Use with --action=edit or
--action=create
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate
in expo-updates' configuration. Defaults to a file named "private-key.pem" in
the certificate's directory.
--runtime-version=<value> Runtime version to target. Use with --action=create
--branch=<value> branch to rollout
--end end the rollout
--json Enable JSON output, non-JSON messages will be printed to stderr.
--non-interactive Run the command in non-interactive mode.
--percent=<value> percent of users to send to the new branch
DESCRIPTION
Roll a new branch out on a channel incrementally.
```
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/channel/rollout.ts)_
_See code: [src/commands/channel/rollout.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/channel/rollout.ts)_
## `eas channel:view [NAME]`
@@ -728,7 +717,7 @@ DESCRIPTION
view a channel
```
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/channel/view.ts)_
_See code: [src/commands/channel/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/channel/view.ts)_
## `eas config`
@@ -749,7 +738,7 @@ DESCRIPTION
display project configuration (app.json + eas.json)
```
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/config.ts)_
_See code: [src/commands/config.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/config.ts)_
## `eas credentials`
@@ -766,7 +755,7 @@ DESCRIPTION
manage credentials
```
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/credentials.ts)_
_See code: [src/commands/credentials.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/credentials.ts)_
## `eas device:create`
@@ -780,7 +769,7 @@ DESCRIPTION
register new Apple Devices to use for internal distribution
```
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/device/create.ts)_
_See code: [src/commands/device/create.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/device/create.ts)_
## `eas device:delete`
@@ -800,7 +789,7 @@ DESCRIPTION
remove a registered device from your account
```
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/device/delete.ts)_
_See code: [src/commands/device/delete.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/device/delete.ts)_
## `eas device:list`
@@ -821,7 +810,7 @@ DESCRIPTION
list all registered devices for your account
```
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/device/list.ts)_
_See code: [src/commands/device/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/device/list.ts)_
## `eas device:rename`
@@ -842,7 +831,7 @@ DESCRIPTION
rename a registered device
```
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/device/rename.ts)_
_See code: [src/commands/device/rename.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/device/rename.ts)_
## `eas device:view [UDID]`
@@ -856,7 +845,7 @@ DESCRIPTION
view a device for your project
```
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/device/view.ts)_
_See code: [src/commands/device/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/device/view.ts)_
## `eas diagnostics`
@@ -870,7 +859,7 @@ DESCRIPTION
display environment info
```
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/diagnostics.ts)_
_See code: [src/commands/diagnostics.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/diagnostics.ts)_
## `eas help [COMMAND]`
@@ -958,7 +947,7 @@ DESCRIPTION
validate the local store configuration
```
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/metadata/lint.ts)_
_See code: [src/commands/metadata/lint.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/metadata/lint.ts)_
## `eas metadata:pull`
@@ -975,7 +964,7 @@ DESCRIPTION
generate the local store configuration from the app stores
```
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/metadata/pull.ts)_
_See code: [src/commands/metadata/pull.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/metadata/pull.ts)_
## `eas metadata:push`
@@ -992,7 +981,7 @@ DESCRIPTION
sync the local store configuration to the app stores
```
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/metadata/push.ts)_
_See code: [src/commands/metadata/push.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/metadata/push.ts)_
## `eas open`
@@ -1006,7 +995,7 @@ DESCRIPTION
open the project page in a web browser
```
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/open.ts)_
_See code: [src/commands/open.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/open.ts)_
## `eas project:info`
@@ -1020,7 +1009,7 @@ DESCRIPTION
information about the current project
```
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/project/info.ts)_
_See code: [src/commands/project/info.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/project/info.ts)_
## `eas project:init`
@@ -1042,7 +1031,7 @@ ALIASES
$ eas init
```
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/project/init.ts)_
_See code: [src/commands/project/init.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/project/init.ts)_
## `eas secret:create`
@@ -1065,7 +1054,7 @@ DESCRIPTION
create an environment secret on the current project or owner account
```
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/secret/create.ts)_
_See code: [src/commands/secret/create.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/secret/create.ts)_
## `eas secret:delete`
@@ -1083,7 +1072,7 @@ DESCRIPTION
delete an environment secret by ID
```
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/secret/delete.ts)_
_See code: [src/commands/secret/delete.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/secret/delete.ts)_
## `eas secret:list`
@@ -1097,7 +1086,7 @@ DESCRIPTION
list environment secrets available for your current app
```
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/secret/list.ts)_
_See code: [src/commands/secret/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/secret/list.ts)_
## `eas secret:push`
@@ -1117,7 +1106,7 @@ DESCRIPTION
read environment secrets from env file and store on the server
```
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/secret/push.ts)_
_See code: [src/commands/secret/push.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/secret/push.ts)_
## `eas submit`
@@ -1147,7 +1136,7 @@ ALIASES
$ eas build:submit
```
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/submit.ts)_
_See code: [src/commands/submit.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/submit.ts)_
## `eas update`
@@ -1155,21 +1144,20 @@ publish an update group
```
USAGE
$ eas update [--branch <value>] [--channel <value>] [-m <value>] [--republish | --input-dir <value> |
--skip-bundler] [--group <value> | | ] [--clear-cache] [-p android|ios|all] [--dev] [--auto] [--private-key-path
<value>] [--json --non-interactive]
$ eas update [--branch <value>] [--channel <value>] [--message <value>] [--republish | --input-dir <value>
| --skip-bundler] [--group <value> | | ] [--clear-cache] [-p android|ios|all] [--auto] [--private-key-path <value>]
[--json --non-interactive]
FLAGS
-m, --message=<value> A short message describing the update
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the update group on
--channel=<value> Channel that the published update should affect
--clear-cache Clear the bundler cache before publishing
--dev Publish a development bundle
--group=<value> Update group to republish (deprecated, see republish command)
--input-dir=<value> [default: dist] Location of the bundle
--json Enable JSON output, non-JSON messages will be printed to stderr.
--message=<value> A short message describing the update
--non-interactive Run the command in non-interactive mode.
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
@@ -1181,7 +1169,7 @@ DESCRIPTION
publish an update group
```
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/index.ts)_
_See code: [src/commands/update/index.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/index.ts)_
## `eas update:configure`
@@ -1199,7 +1187,7 @@ DESCRIPTION
configure the project to support EAS Update
```
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/configure.ts)_
_See code: [src/commands/update/configure.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/configure.ts)_
## `eas update:delete GROUPID`
@@ -1220,7 +1208,7 @@ DESCRIPTION
delete all the updates in an update group
```
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/delete.ts)_
_See code: [src/commands/update/delete.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/delete.ts)_
## `eas update:list`
@@ -1242,7 +1230,7 @@ DESCRIPTION
view the recent updates
```
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/list.ts)_
_See code: [src/commands/update/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/list.ts)_
## `eas update:republish`
@@ -1250,72 +1238,23 @@ roll back to an existing update
```
USAGE
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [-m <value>] [-p android|ios|all]
[--private-key-path <value>] [--json --non-interactive]
$ eas update:republish [--channel <value> | --branch <value> | --group <value>] [--message <value>] [-p
android|ios|all] [--json --non-interactive]
FLAGS
-m, --message=<value> Short message describing the republished update group
-p, --platform=(android|ios|all) [default: all]
--branch=<value> Branch name to select an update group to republish from
--channel=<value> Channel name to select an update group to republish from
--branch=<value> Branch name to select an update to republish from
--channel=<value> Channel name to select an update to republish from
--group=<value> Update group ID to republish
--json Enable JSON output, non-JSON messages will be printed to stderr.
--message=<value> Short message describing the republished update
--non-interactive Run the command in non-interactive mode.
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
DESCRIPTION
roll back to an existing update
```
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:roll-back-to-embedded`
roll back to the embedded update
```
USAGE
$ eas update:roll-back-to-embedded [--branch <value>] [--channel <value>] [--message <value>] [-p android|ios|all] [--auto]
[--private-key-path <value>] [--json --non-interactive]
FLAGS
-p, --platform=(android|ios|all) [default: all]
--auto Use the current git branch and commit message for the EAS branch and update message
--branch=<value> Branch to publish the rollback to embedded update group on
--channel=<value> Channel that the published rollback to embedded update should affect
--json Enable JSON output, non-JSON messages will be printed to stderr.
--message=<value> A short message describing the rollback to embedded update
--non-interactive Run the command in non-interactive mode.
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
DESCRIPTION
roll back to the embedded update
```
_See code: [src/commands/update/roll-back-to-embedded.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/roll-back-to-embedded.ts)_
## `eas update:rollback`
roll back to an embedded update or an existing update
```
USAGE
$ eas update:rollback [--private-key-path <value>]
FLAGS
--private-key-path=<value> File containing the PEM-encoded private key corresponding to the certificate in
expo-updates' configuration. Defaults to a file named "private-key.pem" in the
certificate's directory.
DESCRIPTION
roll back to an embedded update or an existing update
```
_See code: [src/commands/update/rollback.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/rollback.ts)_
_See code: [src/commands/update/republish.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/republish.ts)_
## `eas update:view GROUPID`
@@ -1335,7 +1274,7 @@ DESCRIPTION
update group details
```
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/update/view.ts)_
_See code: [src/commands/update/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/update/view.ts)_
## `eas webhook:create`
@@ -1356,7 +1295,7 @@ DESCRIPTION
create a webhook
```
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/webhook/create.ts)_
_See code: [src/commands/webhook/create.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/webhook/create.ts)_
## `eas webhook:delete [ID]`
@@ -1376,7 +1315,7 @@ DESCRIPTION
delete a webhook
```
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/webhook/delete.ts)_
_See code: [src/commands/webhook/delete.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/webhook/delete.ts)_
## `eas webhook:list`
@@ -1394,7 +1333,7 @@ DESCRIPTION
list webhooks
```
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/webhook/list.ts)_
_See code: [src/commands/webhook/list.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/webhook/list.ts)_
## `eas webhook:update`
@@ -1416,7 +1355,7 @@ DESCRIPTION
update a webhook
```
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/webhook/update.ts)_
_See code: [src/commands/webhook/update.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/webhook/update.ts)_
## `eas webhook:view ID`
@@ -1433,7 +1372,7 @@ DESCRIPTION
view a webhook
```
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v5.9.0/packages/eas-cli/src/commands/webhook/view.ts)_
_See code: [src/commands/webhook/view.ts](https://github.com/expo/eas-cli/blob/v3.14.0/packages/eas-cli/src/commands/webhook/view.ts)_
## `eas whoami`
@@ -1449,4 +1388,5 @@ DESCRIPTION
ALIASES
$ eas whoami
```
<!-- commandsstop -->
+369 -4167
View File
File diff suppressed because it is too large. Load diff
+15 -15
View File
@@ -1,39 +1,39 @@
{
"name": "eas-cli",
"description": "EAS command line tool",
"version": "5.9.0",
"version": "3.14.0",
"author": "Expo <[email protected]>",
"bin": {
"eas": "./bin/run"
},
"bugs": "https://github.com/expo/eas-cli/issues",
"dependencies": {
"@expo/apple-utils": "1.3.2",
"@expo/apple-utils": "1.3.1",
"@expo/code-signing-certificates": "0.0.5",
"@expo/config": "8.1.2",
"@expo/config-plugins": "7.2.4",
"@expo/config-plugins": "7.2.3",
"@expo/config-types": "49.0.0",
"@expo/eas-build-job": "1.0.48",
"@expo/eas-json": "5.7.0",
"@expo/eas-build-job": "1.0.13",
"@expo/eas-json": "3.13.0",
"@expo/json-file": "8.2.37",
"@expo/multipart-body-parser": "1.1.0",
"@expo/osascript": "2.0.33",
"@expo/package-manager": "1.1.2",
"@expo/package-manager": "0.0.57",
"@expo/pkcs12": "0.0.8",
"@expo/plist": "0.0.20",
"@expo/plugin-help": "5.1.22",
"@expo/plugin-warn-if-update-available": "2.5.0",
"@expo/prebuild-config": "6.2.5",
"@expo/prebuild-config": "5.0.7",
"@expo/results": "1.0.0",
"@expo/rudder-sdk-node": "1.1.1",
"@expo/spawn-async": "1.7.0",
"@expo/steps": "1.0.34",
"@expo/steps": "1.0.23",
"@expo/timeago.js": "1.0.0",
"@oclif/core": "1.23.2",
"@oclif/plugin-autocomplete": "1.3.10",
"@segment/ajv-human-errors": "^2.1.2",
"@urql/core": "4.0.11",
"@urql/exchange-retry": "1.2.0",
"@urql/core": "3.1.1",
"@urql/exchange-retry": "1.0.0",
"ajv": "8.11.0",
"ajv-formats": "2.1.1",
"better-opn": "3.0.2",
@@ -47,10 +47,11 @@
"fast-glob": "3.2.12",
"figures": "3.2.0",
"form-data": "4.0.0",
"freeport-async": "2.0.0",
"fs-extra": "10.1.0",
"getenv": "1.0.0",
"gradle-to-js": "2.0.1",
"graphql": "16.8.1",
"graphql": "16.6.0",
"graphql-tag": "2.12.6",
"https-proxy-agent": "5.0.1",
"ignore": "5.2.4",
@@ -73,7 +74,7 @@
"prompts": "2.4.2",
"qrcode-terminal": "0.12.0",
"resolve-from": "5.0.0",
"semver": "7.5.2",
"semver": "7.3.8",
"slash": "3.0.0",
"tar": "6.1.13",
"terminal-link": "2.1.1",
@@ -204,12 +205,11 @@
"scripts": {
"postpack": "rimraf oclif.manifest.json",
"prepack": "yarn rebuild && node ./scripts/prepack.js",
"rebuild": "rimraf build && yarn build-ci",
"rebuild": "rimraf build && yarn build",
"pretarball-ci": "./scripts/pretarball-ci.sh",
"build": "tsc --project tsconfig.build.json",
"build-ci": "tsc --project tsconfig.build-ci.json",
"watch": "yarn build --watch --preserveWatchOutput",
"typecheck": "tsc --project tsconfig.build-ci.json",
"typecheck": "tsc",
"test": "jest",
"version": "yarn oclif readme && node scripts/patch-readme && git add README.md",
"generate-graphql-code": "graphql-codegen --config graphql-codegen.yml",
+12
View File
@@ -2,6 +2,7 @@ import { JSONValue } from '@expo/json-file';
import { ApiV2Error } from './ApiV2Error';
import fetch, { RequestError, RequestInit } from './fetch';
import { getFreePortAsync } from './utils/port';
interface RequestOptions {
body: JSONValue;
@@ -104,3 +105,14 @@ export function getEASUpdateURL(projectId: string): string {
return new URL(projectId, `https://u.expo.dev`).href;
}
}
export async function getSsoLocalServerPortAsync(): Promise<number> {
let startPort: number;
if (process.env.SSO_LOCAL_SERVER_PORT) {
startPort = Number(process.env.SSO_LOCAL_SERVER_PORT);
} else {
startPort = 19300;
}
const port = await getFreePortAsync(startPort);
return port;
}
@@ -1,12 +0,0 @@
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
export const testBasicBranchInfo: UpdateBranchBasicInfoFragment = {
id: 'c4fec95d-7ab1-4d35-bfe6-fe7ff05beda9',
name: 'development',
__typename: 'UpdateBranch',
};
export const testBasicBranchInfo2: UpdateBranchBasicInfoFragment = {
id: '43ccb8a4-8aaa-47f0-9790-dcc80933441b',
name: 'preview',
__typename: 'UpdateBranch',
};
@@ -1,107 +0,0 @@
import { EASUpdateAction, EASUpdateContext, NonInteractiveError } from '../../eas-update/utils';
import { UpdateBranchBasicInfoFragment } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import {
Connection,
Edge,
FilterPagination,
QueryParams,
selectPaginatedAsync,
} from '../../utils/relay';
/**
* Select a branch for the project.
*
* @constructor
* @param {function} options.filterPredicate - A predicate to filter the branches that are shown to the user. It takes a branchInfo object as a parameter and returns a boolean.
* @param {string} options.printedType - The type of branch printed to the user. Defaults to 'branch'.
* @param {number} options.pageSize - The number of branches to show per page. Defaults to 100.
* @param {function} options.beforeEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called before each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @param {function} options.afterEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called after each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @args willFetchAgain If the query will fetch again to get a complete page.
*/
export class SelectBranch implements EASUpdateAction<UpdateBranchBasicInfoFragment | null> {
constructor(
private options: {
filterPredicate?: (branchInfo: UpdateBranchBasicInfoFragment) => boolean;
printedType?: string;
pageSize?: number;
beforeEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateBranchBasicInfoFragment>[]
) => void;
afterEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateBranchBasicInfoFragment>[],
willFetchAgain: boolean
) => void;
} = {}
) {}
async queryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
return await BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync(graphqlClient, {
appId: projectId,
...queryParams,
});
}
async filterQueryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams,
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) => boolean
): Promise<Connection<UpdateBranchBasicInfoFragment>> {
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
await this.queryAsync(ctx, queryParams);
return await FilterPagination.getPageAsync({
queryParams,
queryAsync,
filterPredicate,
beforeEachQuery: this.options.beforeEachFilterQuery,
afterEachQuery: this.options.afterEachFilterQuery,
});
}
public async runAsync(ctx: EASUpdateContext): Promise<UpdateBranchBasicInfoFragment | null> {
const { nonInteractive } = ctx;
const { filterPredicate } = this.options;
const printedType = this.options.printedType ?? 'branch';
const pageSize = this.options.pageSize ?? 100;
if (nonInteractive) {
throw new NonInteractiveError(
`${printedType} selection cannot be run in non-interactive mode.`
);
}
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateBranchBasicInfoFragment>> =>
filterPredicate
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
: this.queryAsync(ctx, queryParams);
const getTitleAsync = async (branchInfo: UpdateBranchBasicInfoFragment): Promise<string> =>
branchInfo.name;
return await selectPaginatedAsync({
queryAsync,
getTitleAsync,
printedType,
pageSize,
});
}
}
@@ -1,92 +0,0 @@
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
import { NonInteractiveError } from '../../../eas-update/utils';
import { UpdateBranchBasicInfoFragment } from '../../../graphql/generated';
import { BranchQuery } from '../../../graphql/queries/BranchQuery';
import { promptAsync } from '../../../prompts';
import { testBasicBranchInfo, testBasicBranchInfo2 } from '../../__tests__/fixtures';
import { SelectBranch } from '../SelectBranch';
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/BranchQuery');
describe(SelectBranch, () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns null if project has no branchs', async () => {
const ctx = createCtxMock();
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBeNull();
});
it('still prompts if project has only one branch', async () => {
const ctx = createCtxMock();
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [{ node: testBasicBranchInfo, cursor: 'cursor' }],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicBranchInfo,
}));
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
expect(promptAsync).toHaveBeenCalledTimes(1);
});
it('returns the selected branch', async () => {
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicBranchInfo, cursor: 'cursor' },
{ node: testBasicBranchInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicBranchInfo,
}));
const ctx = createCtxMock();
const selectBranch = new SelectBranch();
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
});
it('returns the only available branch with a filter', async () => {
jest.mocked(BranchQuery.listBranchesBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicBranchInfo, cursor: 'cursor' },
{ node: testBasicBranchInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const ctx = createCtxMock();
const selectBranch = new SelectBranch({
filterPredicate: (branchInfo: UpdateBranchBasicInfoFragment) =>
branchInfo === testBasicBranchInfo,
});
const branch = await selectBranch.runAsync(ctx);
expect(branch).toBe(testBasicBranchInfo);
});
it('throws an error in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const selectBranch = new SelectBranch();
// dont fail if users are running in non-interactive mode
await expect(selectBranch.runAsync(ctx)).rejects.toThrowError(NonInteractiveError);
});
});
+4 -3
View File
@@ -1,8 +1,9 @@
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
export async function getDefaultBranchNameAsync(vcsClient: Client): Promise<string> {
export async function getDefaultBranchNameAsync(): Promise<string> {
return (
(await vcsClient.getBranchNameAsync()) || `branch-${Math.random().toString(36).substring(2, 4)}`
(await getVcsClient().getBranchNameAsync()) ||
`branch-${Math.random().toString(36).substring(2, 4)}`
);
}
@@ -13,8 +13,6 @@ import {
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildFreeTierIosLimitExceededError,
EasBuildFreeTierLimitExceededError,
EasBuildResourceClassNotAvailableInFreeTierError,
EasBuildTooManyPendingBuildsError,
RequestValidationError,
@@ -27,18 +25,12 @@ beforeEach(() => {
const EXPECTED_STRINGIFIED_GRAPHQL_ERROR_JSON = `[
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 1"
},
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 2"
},
{
"name": "GraphQLError",
"extensions": {},
"message": "Error 3"
}
]`;
@@ -288,45 +280,6 @@ describe(Build.name, () => {
);
});
});
describe('for EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError = getError<EasBuildFreeTierLimitExceededError>(
() => {
handleBuildRequestError(error, platform);
}
);
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierLimitExceededError,
'Error 1'
);
});
});
describe('for EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED', () => {
it('throws correct EasCommandError subclass', async () => {
const platform = Platform.ANDROID;
const graphQLError = getGraphQLError('Error 1', 'EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED');
const graphQLErrors = [graphQLError];
const error = new CombinedError({ graphQLErrors });
const handleBuildRequestErrorThrownError =
getError<EasBuildFreeTierIosLimitExceededError>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(
handleBuildRequestErrorThrownError,
EasBuildFreeTierIosLimitExceededError,
'Error 1'
);
});
});
describe('for VALIDATION_ERROR', () => {
describe('with Android', () => {
@@ -471,29 +424,6 @@ describe(Build.name, () => {
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
const platform = Platform.ANDROID;
const graphQLErrors = [
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
];
const error = new CombinedError({ graphQLErrors });
const expectedMessage =
`${EXPECTED_GENERIC_MESSAGE}\n` +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 1\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 2\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 3';
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
describe('without request ID', () => {
it('throws base Error class with custom message without request ID line', async () => {
const platform = Platform.ANDROID;
@@ -525,29 +455,6 @@ describe(Build.name, () => {
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
it('throws base Error class with message including all error messages if combined error has multiple GraphQL errors', async () => {
const platform = Platform.IOS;
const graphQLErrors = [
getGraphQLError('Error 1', 'UNKNOWN_GRAPHQL_ERROR'),
getGraphQLError('Error 2', 'OTHER_GRAPHQL_ERROR'),
getGraphQLError('Error 3', 'YET_ANOTHER_GRAPHQL_ERROR'),
];
const error = new CombinedError({ graphQLErrors });
const expectedMessage =
`${EXPECTED_GENERIC_MESSAGE}\n` +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 1\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 2\n' +
`Request ID: ${mockRequestId}\n` +
'Error message: Error 3';
const handleBuildRequestErrorThrownError = getError<Error>(() => {
handleBuildRequestError(error, platform);
});
assertReThrownError(handleBuildRequestErrorThrownError, Error, expectedMessage);
});
describe('without request ID', () => {
it('throws base Error class with custom message without request ID line', async () => {
const platform = Platform.IOS;
@@ -8,7 +8,6 @@ import os from 'os';
import path from 'path';
import { getAppBuildGradleAsync, resolveConfigValue } from '../../../project/android/gradleUtils';
import { getVcsClient } from '../../../vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -19,8 +18,6 @@ import {
const fsReal = jest.requireActual('fs').promises as typeof fs;
jest.mock('fs');
const vcsClient = getVcsClient();
afterAll(async () => {
// do not remove the following line
// this fixes a weird error with tempy in @expo/image-utils
@@ -194,8 +191,7 @@ describe(maybeResolveVersionsAsync, () => {
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('3.0.0');
expect(appBuildVersion).toBe('123');
@@ -207,8 +203,7 @@ describe(maybeResolveVersionsAsync, () => {
const { appVersion, appBuildVersion } = await maybeResolveVersionsAsync(
'/app',
exp,
{} as BuildProfile<Platform.ANDROID>,
vcsClient
{} as BuildProfile<Platform.ANDROID>
);
expect(appVersion).toBe('5.0.0');
expect(appBuildVersion).toBe('126');
+4 -15
View File
@@ -59,22 +59,13 @@ This means that it will most likely produce an AAB and you will not be able to i
await checkGoogleServicesFileAsync(ctx);
await validatePNGsForManagedProjectAsync(ctx);
const gradleContext = await resolveGradleBuildContextAsync(
ctx.projectDir,
buildProfile,
ctx.vcsClient
);
const gradleContext = await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
if (ctx.workflow === Workflow.MANAGED) {
await ensureApplicationIdIsDefinedForManagedProjectAsync(ctx);
}
const applicationId = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
gradleContext
);
const applicationId = await getApplicationIdAsync(ctx.projectDir, ctx.exp, gradleContext);
const versionCodeOverride =
ctx.easJsonCliConfig?.appVersionSource === AppVersionSource.REMOTE
? await resolveRemoteVersionCodeAsync(ctx.graphqlClient, {
@@ -83,7 +74,6 @@ This means that it will most likely produce an AAB and you will not be able to i
exp: ctx.exp,
applicationId,
buildProfile,
vcsClient: ctx.vcsClient,
})
: undefined;
@@ -107,7 +97,6 @@ export async function prepareAndroidBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
vcsClient: ctx.vcsClient,
});
},
prepareJobAsync: async (
@@ -135,7 +124,8 @@ export async function prepareAndroidBuildAsync(
}
function shouldProvideCredentials(ctx: BuildContext<Platform.ANDROID>): boolean {
return !ctx.buildProfile.withoutCredentials;
const isCustomBuild = ctx.buildProfile.config !== undefined;
return !ctx.buildProfile.withoutCredentials && !isCustomBuild;
}
async function ensureAndroidCredentialsAsync(
@@ -147,7 +137,6 @@ async function ensureAndroidCredentialsAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
ctx.android.gradleContext
);
const provider = new AndroidCredentialsProvider(ctx.credentialsCtx, {
@@ -14,6 +14,7 @@ import slash from 'slash';
import { AndroidCredentials } from '../../credentials/android/AndroidCredentialsProvider';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -33,7 +34,7 @@ export async function prepareJobAsync(
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.ANDROID> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const { credentials } = jobData;
const buildCredentials = credentials
? {
@@ -65,8 +66,6 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
ndk: buildProfile.ndk,
expoCli: buildProfile.expoCli,
@@ -11,7 +11,6 @@ import Log from '../../log';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import { Client } from '../../vcs/vcs';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync(
@@ -21,16 +20,14 @@ export async function syncProjectConfigurationAsync(
exp,
localAutoIncrement,
projectId,
vcsClient,
}: {
projectDir: string;
exp: ExpoConfig;
localAutoIncrement?: AndroidVersionAutoIncrement;
projectId: string;
vcsClient: Client;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
@@ -18,7 +18,6 @@ import {
} from '../../project/android/gradleUtils';
import { getNextVersionCode } from '../../project/android/versions';
import { resolveWorkflowAsync } from '../../project/workflow';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
@@ -97,10 +96,9 @@ export async function bumpVersionInAppJsonAsync({
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
buildProfile: BuildProfile<Platform.ANDROID>,
vcsClient: Client
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
const buildGradle = await getAppBuildGradleAsync(projectDir);
try {
@@ -185,14 +183,12 @@ export async function resolveRemoteVersionCodeAsync(
exp,
applicationId,
buildProfile,
vcsClient,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationId: string;
buildProfile: BuildProfile<Platform.ANDROID>;
vcsClient: Client;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
@@ -202,7 +198,7 @@ export async function resolveRemoteVersionCodeAsync(
applicationId
);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile, vcsClient);
const localVersions = await maybeResolveVersionsAsync(projectDir, exp, buildProfile);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
currentBuildVersion = remoteVersions.buildVersion;
+95 -30
View File
@@ -1,10 +1,9 @@
import { ArchiveSource, ArchiveSourceType, Job, Metadata, Platform } from '@expo/eas-build-job';
import { CredentialsSource } from '@expo/eas-json';
import { CredentialsSource, EasJsonAccessor, EasJsonUtils, ResourceClass } from '@expo/eas-json';
import assert from 'assert';
import chalk from 'chalk';
import cliProgress from 'cli-progress';
import fs from 'fs-extra';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import { BuildEvent } from '../analytics/AnalyticsManager';
@@ -17,6 +16,7 @@ import {
BuildFragment,
BuildParamsInput,
BuildPriority,
BuildResourceClass,
BuildStatus,
UploadSessionType,
} from '../graphql/generated';
@@ -29,20 +29,19 @@ import {
appPlatformEmojis,
requestedPlatformDisplayNames,
} from '../platform';
import { confirmAsync } from '../prompts';
import { uploadFileAtPathToGCSAsync } from '../uploads';
import { formatBytes } from '../utils/files';
import { printJsonOnlyOutput } from '../utils/json';
import { createProgressTracker } from '../utils/progress';
import { sleepAsync } from '../utils/promise';
import { getVcsClient } from '../vcs';
import { BuildContext } from './context';
import {
EasBuildDownForMaintenanceError,
EasBuildFreeTierDisabledAndroidError,
EasBuildFreeTierDisabledError,
EasBuildFreeTierDisabledIOSError,
EasBuildFreeTierIosLimitExceededError,
EasBuildFreeTierLimitExceededError,
EasBuildLegacyResourceClassNotAvailableError,
EasBuildProjectArchiveUploadError,
EasBuildResourceClassNotAvailableInFreeTierError,
EasBuildTooManyPendingBuildsError,
@@ -122,10 +121,9 @@ export async function prepareBuildRequestForPlatformAsync<
}
);
if (await ctx.vcsClient.isCommitRequiredAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.addNewLineIfNone();
await reviewAndCommitChangesAsync(
ctx.vcsClient,
`[EAS Build] Run EAS Build for ${requestedPlatformDisplayNames[ctx.platform as Platform]}`,
{ nonInteractive: ctx.nonInteractive }
);
@@ -135,7 +133,7 @@ export async function prepareBuildRequestForPlatformAsync<
if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.LOCAL_BUILD_PLUGIN) {
projectArchive = {
type: ArchiveSourceType.PATH,
path: (await makeProjectTarballAsync(ctx.vcsClient)).path,
path: (await makeProjectTarballAsync()).path,
};
} else if (ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL) {
projectArchive = {
@@ -185,11 +183,8 @@ const SERVER_SIDE_DEFINED_ERRORS: Record<string, typeof EasCommandError> = {
EAS_BUILD_FREE_TIER_DISABLED: EasBuildFreeTierDisabledError,
EAS_BUILD_FREE_TIER_DISABLED_IOS: EasBuildFreeTierDisabledIOSError,
EAS_BUILD_FREE_TIER_DISABLED_ANDROID: EasBuildFreeTierDisabledAndroidError,
EAS_BUILD_FREE_TIER_LIMIT_EXCEEDED: EasBuildFreeTierLimitExceededError,
EAS_BUILD_FREE_TIER_IOS_LIMIT_EXCEEDED: EasBuildFreeTierIosLimitExceededError,
EAS_BUILD_RESOURCE_CLASS_NOT_AVAILABLE_IN_FREE_TIER:
EasBuildResourceClassNotAvailableInFreeTierError,
EAS_BUILD_LEGACY_RESOURCE_CLASS_NOT_AVAILABLE: EasBuildLegacyResourceClassNotAvailableError,
VALIDATION_ERROR: RequestValidationError,
};
@@ -211,19 +206,15 @@ export function handleBuildRequestError(error: any, platform: Platform): never {
`You have already reached the maximum number of pending ${requestedPlatformDisplayNames[platform]} builds for your account. Try again later.`
);
} else if (error?.graphQLErrors) {
const errorMessage = error.graphQLErrors
.map((graphQLError: GraphQLError) => {
const requestIdLine = graphQLError?.extensions?.requestId
? `\nRequest ID: ${graphQLError.extensions.requestId}`
: '';
const errorMessageLine = graphQLError?.message
? `\nError message: ${graphQLError.message}`
: '';
return `${requestIdLine}${errorMessageLine}`;
})
.join('');
const graphQLError = error.graphQLErrors[0];
const requestIdLine = graphQLError?.extensions?.requestId
? `\nRequest ID: ${graphQLError.extensions.requestId}`
: '';
const originalErrorMessage = graphQLError?.message
? `\nError message: ${graphQLError.message}`
: '';
throw new Error(
`Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.${errorMessage}`
`Build request failed. Make sure you are using the latest eas-cli version. If the problem persists, report the issue.${requestIdLine}${originalErrorMessage}`
);
}
throw error;
@@ -243,7 +234,7 @@ async function uploadProjectAsync<TPlatform extends Platform>(
'https://expo.fyi/eas-build-archive'
)}`
);
const projectTarball = await makeProjectTarballAsync(ctx.vcsClient);
const projectTarball = await makeProjectTarballAsync();
if (projectTarball.size > 1024 * 1024 * 100) {
Log.warn(
@@ -255,10 +246,6 @@ async function uploadProjectAsync<TPlatform extends Platform>(
);
}
if (projectTarball.size > 2 * 1024 * 1024 * 1024) {
throw new Error('Project archive is too big. Maximum allowed size is 2GB.');
}
projectTarballPath = projectTarball.path;
const bucketKey = await uploadFileAtPathToGCSAsync(
@@ -333,7 +320,12 @@ export type MaybeBuildFragment = BuildFragment | null;
export async function waitForBuildEndAsync(
graphqlClient: ExpoGraphqlClient,
{ buildIds, accountName }: { buildIds: string[]; accountName: string },
{
buildIds,
accountName,
projectDir,
nonInteractive,
}: { buildIds: string[]; accountName: string; projectDir: string; nonInteractive: boolean },
{ intervalSec = 10 } = {}
): Promise<MaybeBuildFragment[]> {
let spinner;
@@ -350,7 +342,10 @@ export async function waitForBuildEndAsync(
const builds = await getBuildsSafelyAsync(graphqlClient, buildIds);
const { refetch } =
builds.length === 1
? await handleSingleBuildProgressAsync({ build: builds[0], accountName }, { spinner })
? await handleSingleBuildProgressAsync(
{ build: builds[0], accountName, projectDir, nonInteractive },
{ spinner }
)
: await handleMultipleBuildsProgressAsync({ builds }, { spinner, originalSpinnerText });
if (!refetch) {
return builds;
@@ -388,9 +383,13 @@ async function handleSingleBuildProgressAsync(
{
build,
accountName,
projectDir,
nonInteractive,
}: {
build: MaybeBuildFragment;
accountName: string;
projectDir: string;
nonInteractive: boolean;
},
{ spinner }: { spinner: Ora }
): Promise<BuildProgressResult> {
@@ -444,6 +443,54 @@ async function handleSingleBuildProgressAsync(
);
}
if (
build.platform === AppPlatform.Ios &&
[BuildResourceClass.IosIntelLarge, BuildResourceClass.IosIntelMedium].includes(
build.resourceClass
)
) {
let askToSwitchToM1 = false;
if (
build.priority === BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 10 minutes */ 10 * 60
) {
Log.newLine();
Log.warn(
`Warning: Priority queue wait time for legacy iOS Intel workers is longer than usual (more than 10 minutes).`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
} else if (
build.priority !== BuildPriority.High &&
build.estimatedWaitTimeLeftSeconds >= /* 120 minutes */ 120 * 60
) {
Log.newLine();
Log.warn(
`Warning: Free tier queue wait time for legacy iOS Intel workers is longer than usual.`
);
Log.warn(`We recommend switching to the new, faster M1 workers for iOS builds.`);
Log.warn(
learnMore('https://blog.expo.dev/m1-workers-on-eas-build-dcaa2c1333ad', {
learnMoreMessage: 'Learn more on switching to M1 workers.',
})
);
askToSwitchToM1 = true;
}
if (!nonInteractive && askToSwitchToM1) {
const shouldSwitchToM1 = await confirmAsync({
message: `Switch iOS builds to M1 workers (modifies build profiles in eas.json)?`,
});
if (shouldSwitchToM1) {
await updateIosBuildProfilesToUseM1WorkersAsync(projectDir);
}
}
}
Log.newLine();
Log.log(`Waiting in ${priorityToQueueDisplayName[build.priority]}`);
queueProgressBar.start(
@@ -592,3 +639,21 @@ function formatAccountSubscriptionsUrl(accountName: string): string {
getExpoWebsiteBaseUrl()
).toString();
}
async function updateIosBuildProfilesToUseM1WorkersAsync(projectDir: string): Promise<void> {
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
await easJsonAccessor.readRawJsonAsync();
const profileNames = await EasJsonUtils.getBuildProfileNamesAsync(easJsonAccessor);
easJsonAccessor.patch(easJsonRawObject => {
for (const profileName of profileNames) {
easJsonRawObject.build[profileName].ios = {
...easJsonRawObject.build[profileName].ios,
resourceClass: ResourceClass.M_MEDIUM,
};
}
return easJsonRawObject;
});
await easJsonAccessor.writeAsync();
Log.withTick('Updated eas.json. Your next builds will run on M1 workers.');
}
+10 -15
View File
@@ -6,13 +6,12 @@ import fs from 'fs-extra';
import Log, { learnMore } from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
import { maybeBailOnRepoStatusAsync, reviewAndCommitChangesAsync } from './utils/repository';
interface ConfigureParams {
projectDir: string;
nonInteractive: boolean;
vcsClient: Client;
}
export async function easJsonExistsAsync(projectDir: string): Promise<boolean> {
@@ -37,18 +36,14 @@ export async function ensureProjectConfiguredAsync(
return true;
}
async function configureAsync({
projectDir,
nonInteractive,
vcsClient,
}: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync(vcsClient);
async function configureAsync({ projectDir, nonInteractive }: ConfigureParams): Promise<void> {
await maybeBailOnRepoStatusAsync();
await createEasJsonAsync(projectDir, vcsClient);
await createEasJsonAsync(projectDir);
if (await vcsClient.isCommitRequiredAsync()) {
if (await getVcsClient().isCommitRequiredAsync()) {
Log.newLine();
await reviewAndCommitChangesAsync(vcsClient, 'Configure EAS Build', {
await reviewAndCommitChangesAsync('Configure EAS Build', {
nonInteractive,
});
}
@@ -97,20 +92,20 @@ const EAS_JSON_BARE_DEFAULT: EasJson = {
},
};
async function createEasJsonAsync(projectDir: string, vcsClient: Client): Promise<void> {
async function createEasJsonAsync(projectDir: string): Promise<void> {
const easJsonPath = EasJsonAccessor.formatEasJsonPath(projectDir);
const hasAndroidNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient)) === Workflow.GENERIC;
(await resolveWorkflowAsync(projectDir, Platform.ANDROID)) === Workflow.GENERIC;
const hasIosNativeProject =
(await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient)) === Workflow.GENERIC;
(await resolveWorkflowAsync(projectDir, Platform.IOS)) === Workflow.GENERIC;
const easJson =
hasAndroidNativeProject || hasIosNativeProject
? EAS_JSON_BARE_DEFAULT
: EAS_JSON_MANAGED_DEFAULT;
await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`);
await vcsClient.trackFileAsync(easJsonPath);
await getVcsClient().trackFileAsync(easJsonPath);
Log.withTick(
`Generated ${chalk.bold('eas.json')}. ${learnMore(
'https://docs.expo.dev/build-reference/eas-json/'
-5
View File
@@ -1,7 +1,6 @@
import { ExpoConfig } from '@expo/config';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile, EasJson } from '@expo/eas-json';
import { NodePackageManager } from '@expo/package-manager';
import { Analytics, AnalyticsEventProperties } from '../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
@@ -12,7 +11,6 @@ import { GradleBuildContext } from '../project/android/gradle';
import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
import { XcodeBuildContext } from '../project/ios/scheme';
import { Actor } from '../user/User';
import { Client } from '../vcs/vcs';
import { LocalBuildOptions } from './local';
export type CommonContext<T extends Platform> = Omit<BuildContext<T>, 'android' | 'ios'>;
@@ -57,7 +55,4 @@ export interface BuildContext<T extends Platform> {
customBuildConfigMetadata?: CustomBuildConfigMetadata;
android: T extends Platform.ANDROID ? AndroidBuildContext : undefined;
ios: T extends Platform.IOS ? IosBuildContext : undefined;
developmentClient: boolean;
requiredPackageManager: NodePackageManager['name'] | null;
vcsClient: Client;
}
+1 -17
View File
@@ -1,7 +1,6 @@
import { Platform } from '@expo/eas-build-job';
import { BuildProfile, EasJson, ResourceClass } from '@expo/eas-json';
import JsonFile from '@expo/json-file';
import { resolvePackageManager } from '@expo/package-manager';
import getenv from 'getenv';
import resolveFrom from 'resolve-from';
import { v4 as uuidv4 } from 'uuid';
@@ -14,7 +13,6 @@ import { CustomBuildConfigMetadata } from '../project/customBuildConfig';
import { getOwnerAccountForProjectIdAsync } from '../project/projectUtils';
import { resolveWorkflowAsync } from '../project/workflow';
import { Actor } from '../user/User';
import { Client } from '../vcs/vcs';
import { createAndroidContextAsync } from './android/build';
import { BuildContext, CommonContext } from './context';
import { createIosContextAsync } from './ios/build';
@@ -36,7 +34,6 @@ export async function createBuildContextAsync<T extends Platform>({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
}: {
@@ -54,24 +51,15 @@ export async function createBuildContextAsync<T extends Platform>({
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
}): Promise<BuildContext<T>> {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({ env: buildProfile.env });
const projectName = exp.slug;
const account = await getOwnerAccountForProjectIdAsync(graphqlClient, projectId);
const workflow = await resolveWorkflowAsync(projectDir, platform, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, platform);
const accountId = account.id;
const runFromCI = getenv.boolish('CI', false);
const developmentClient =
buildProfile.developmentClient ??
(platform === Platform.ANDROID
? (buildProfile as BuildProfile<Platform.ANDROID>)?.gradleCommand === ':app:assembleDebug'
: (buildProfile as BuildProfile<Platform.IOS>)?.buildConfiguration === 'Debug') ??
false;
const requiredPackageManager = resolvePackageManager(projectDir);
const credentialsCtx = new CredentialsContext({
projectInfo: { exp, projectId },
@@ -82,7 +70,6 @@ export async function createBuildContextAsync<T extends Platform>({
analytics,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
});
const devClientProperties = getDevClientEventProperties({
@@ -129,13 +116,10 @@ export async function createBuildContextAsync<T extends Platform>({
user: actor,
graphqlClient,
analytics,
vcsClient,
workflow,
message,
runFromCI,
customBuildConfigMetadata,
developmentClient,
requiredPackageManager,
};
if (platform === Platform.ANDROID) {
const common = commonContext as CommonContext<Platform.ANDROID>;
-6
View File
@@ -8,14 +8,8 @@ export class EasBuildFreeTierDisabledIOSError extends EasCommandError {}
export class EasBuildFreeTierDisabledAndroidError extends EasCommandError {}
export class EasBuildFreeTierLimitExceededError extends EasCommandError {}
export class EasBuildFreeTierIosLimitExceededError extends EasCommandError {}
export class EasBuildResourceClassNotAvailableInFreeTierError extends EasCommandError {}
export class EasBuildLegacyResourceClassNotAvailableError extends EasCommandError {}
export class RequestValidationError extends EasCommandError {}
export class EasBuildDownForMaintenanceError extends EasCommandError {}
@@ -6,7 +6,6 @@ import os from 'os';
import type { XCBuildConfiguration } from 'xcode';
import { readPlistAsync } from '../../../utils/plist';
import { getVcsClient } from '../../../vcs';
import {
BumpStrategy,
bumpVersionAsync,
@@ -19,8 +18,6 @@ import {
jest.mock('fs');
const vcsClient = getVcsClient();
const getXCBuildConfigurationFromPbxproj = jest.spyOn(
IOSConfig.Target,
'getXCBuildConfigurationFromPbxproj'
@@ -265,7 +262,7 @@ describe(readBuildNumberAsync, () => {
describe('bare project', () => {
it('reads the build number from native code', async () => {
const exp = initBareWorkflowProject();
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
const buildNumber = await readBuildNumberAsync('/app', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -273,7 +270,7 @@ describe(readBuildNumberAsync, () => {
describe('managed project', () => {
it('reads the build number from expo config', async () => {
const exp = initManagedProject();
const buildNumber = await readBuildNumberAsync('/app', exp, {}, vcsClient);
const buildNumber = await readBuildNumberAsync('/app', exp, {});
expect(buildNumber).toBe('1');
});
});
@@ -283,21 +280,16 @@ describe(readShortVersionAsync, () => {
describe('bare project', () => {
it('reads the short version from native code', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
const appVersion = await readShortVersionAsync('/app', exp, {});
expect(appVersion).toBe('1.0.0');
});
it('evaluates interpolated build number', async () => {
const exp = initBareWorkflowProject({
appVersion: '$(CURRENT_PROJECT_VERSION)',
});
const buildNumber = await readShortVersionAsync(
'/app',
exp,
{
CURRENT_PROJECT_VERSION: '1.0.0',
},
vcsClient
);
const buildNumber = await readShortVersionAsync('/app', exp, {
CURRENT_PROJECT_VERSION: '1.0.0',
});
expect(buildNumber).toBe('1.0.0');
});
@@ -307,14 +299,9 @@ describe(readShortVersionAsync, () => {
});
await expect(
readShortVersionAsync(
'/app',
exp,
{
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
},
vcsClient
)
readShortVersionAsync('/app', exp, {
CURRENT_PROJECT_VERSION: '0.0.7.1.028',
})
).rejects.toThrowError(
'CFBundleShortVersionString (version field in app.json/app.config.js) must be a period-separated list of three non-negative integers. Current value: 0.0.7.1.028'
);
@@ -324,7 +311,7 @@ describe(readShortVersionAsync, () => {
describe('managed project', () => {
it('reads the version from app config', async () => {
const exp = initBareWorkflowProject();
const appVersion = await readShortVersionAsync('/app', exp, {}, vcsClient);
const appVersion = await readShortVersionAsync('/app', exp, {});
expect(appVersion).toBe('1.0.0');
});
});
-4
View File
@@ -39,7 +39,6 @@ export async function createIosContextAsync(
projectDir: ctx.projectDir,
nonInteractive: ctx.nonInteractive,
exp: ctx.exp,
vcsClient: ctx.vcsClient,
},
buildProfile
);
@@ -48,7 +47,6 @@ export async function createIosContextAsync(
exp: ctx.exp,
xcodeBuildContext,
env: buildProfile.env,
vcsClient: ctx.vcsClient,
});
const applicationTarget = findApplicationTarget(targets);
const buildNumberOverride =
@@ -59,7 +57,6 @@ export async function createIosContextAsync(
exp: ctx.exp,
applicationTarget,
buildProfile,
vcsClient: ctx.vcsClient,
})
: undefined;
return {
@@ -89,7 +86,6 @@ export async function prepareIosBuildAsync(
? false
: ctx.buildProfile.autoIncrement,
projectId: ctx.projectId,
vcsClient: ctx.vcsClient,
});
},
prepareJobAsync: async (
@@ -55,5 +55,6 @@ export async function ensureIosCredentialsForBuildResignAsync(
}
function shouldProvideCredentials(buildCtx: BuildContext<Platform.IOS>): boolean {
return !buildCtx.buildProfile.simulator && !buildCtx.buildProfile.withoutCredentials;
const isCustomBuild = buildCtx.buildProfile.config !== undefined;
return !buildCtx.buildProfile.simulator && !isCustomBuild;
}
+2 -3
View File
@@ -16,6 +16,7 @@ import { IosCredentials, TargetCredentials } from '../../credentials/ios/types';
import { IosJobSecretsInput } from '../../graphql/generated';
import { getCustomBuildConfigPath } from '../../project/customBuildConfig';
import { getUsername } from '../../project/projectUtils';
import { getVcsClient } from '../../vcs';
import { BuildContext } from '../context';
interface JobData {
@@ -36,7 +37,7 @@ export async function prepareJobAsync(
const username = getUsername(ctx.exp, ctx.user);
const buildProfile: BuildProfile<Platform.IOS> = ctx.buildProfile;
const projectRootDirectory =
slash(path.relative(await ctx.vcsClient.getRootPathAsync(), ctx.projectDir)) || '.';
slash(path.relative(await getVcsClient().getRootPathAsync(), ctx.projectDir)) || '.';
const buildCredentials: Ios.BuildSecrets['buildCredentials'] = {};
if (jobData.credentials) {
const targetNames = Object.keys(jobData.credentials);
@@ -57,8 +58,6 @@ export async function prepareJobAsync(
builderEnvironment: {
image: buildProfile.image,
node: buildProfile.node,
pnpm: buildProfile.pnpm,
bun: buildProfile.bun,
yarn: buildProfile.yarn,
bundler: buildProfile.bundler,
cocoapods: buildProfile.cocoapods,
@@ -7,7 +7,6 @@ import { Target } from '../../credentials/ios/types';
import { isExpoUpdatesInstalled } from '../../project/projectUtils';
import { resolveWorkflowAsync } from '../../project/workflow';
import { syncUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { Client } from '../../vcs/vcs';
import { BumpStrategy, bumpVersionAsync, bumpVersionInAppJsonAsync } from './version';
export async function syncProjectConfigurationAsync(
@@ -18,22 +17,20 @@ export async function syncProjectConfigurationAsync(
targets,
localAutoIncrement,
projectId,
vcsClient,
}: {
projectDir: string;
exp: ExpoConfig;
targets: Target[];
localAutoIncrement?: IosVersionAutoIncrement;
projectId: string;
vcsClient: Client;
}
): Promise<void> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
const versionBumpStrategy = resolveVersionBumpStrategy(localAutoIncrement ?? false);
if (workflow === Workflow.GENERIC) {
if (isExpoUpdatesInstalled(projectDir)) {
await syncUpdatesConfigurationAsync(graphqlClient, vcsClient, projectDir, exp, projectId);
await syncUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
await bumpVersionAsync({ projectDir, exp, bumpStrategy: versionBumpStrategy, targets });
} else {
+9 -19
View File
@@ -19,7 +19,6 @@ import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import uniqBy from '../../utils/expodash/uniqBy';
import { readPlistAsync, writePlistAsync } from '../../utils/plist';
import { Client } from '../../vcs/vcs';
import { updateAppJsonConfigAsync } from '../utils/appJson';
import { bumpAppVersionAsync, ensureStaticConfigExists } from '../utils/version';
@@ -111,10 +110,9 @@ function validateShortVersion(shortVersion: string | undefined): void {
export async function readShortVersionAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
@@ -133,10 +131,9 @@ export async function readShortVersionAsync(
export async function readBuildNumberAsync(
projectDir: string,
exp: ExpoConfig,
buildSettings: XCBuildConfiguration['buildSettings'],
vcsClient: Client
buildSettings: XCBuildConfiguration['buildSettings']
): Promise<string | undefined> {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
const infoPlist = await readInfoPlistAsync(projectDir, buildSettings);
return (
@@ -150,8 +147,7 @@ export async function readBuildNumberAsync(
export async function maybeResolveVersionsAsync(
projectDir: string,
exp: ExpoConfig,
targets: Target[],
vcsClient: Client
targets: Target[]
): Promise<{ appVersion?: string; appBuildVersion?: string }> {
const applicationTarget = findApplicationTarget(targets);
try {
@@ -159,14 +155,12 @@ export async function maybeResolveVersionsAsync(
appBuildVersion: await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
),
appVersion: await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
),
};
} catch (err: any) {
@@ -284,14 +278,12 @@ export async function resolveRemoteBuildNumberAsync(
exp,
applicationTarget,
buildProfile,
vcsClient,
}: {
projectDir: string;
projectId: string;
exp: ExpoConfig;
applicationTarget: Target;
buildProfile: BuildProfile<Platform.IOS>;
vcsClient: Client;
}
): Promise<string> {
const remoteVersions = await AppVersionQuery.latestVersionAsync(
@@ -304,14 +296,12 @@ export async function resolveRemoteBuildNumberAsync(
const localBuildNumber = await readBuildNumberAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
);
const localShortVersion = await readShortVersionAsync(
projectDir,
exp,
applicationTarget.buildSettings ?? {},
vcsClient
applicationTarget.buildSettings ?? {}
);
let currentBuildVersion: string;
if (remoteVersions?.buildVersion) {
+1 -1
View File
@@ -6,7 +6,7 @@ import semver from 'semver';
import { ora } from '../ora';
const PLUGIN_PACKAGE_NAME = 'eas-cli-local-build-plugin';
const PLUGIN_PACKAGE_VERSION = '1.0.49';
const PLUGIN_PACKAGE_VERSION = '1.0.24';
export enum LocalBuildMode {
/**
+7 -11
View File
@@ -15,6 +15,7 @@ import {
readReleaseChannelSafelyAsync as readIosReleaseChannelSafelyAsync,
} from '../update/ios/UpdatesModule';
import { easCliVersion } from '../utils/easCli';
import { getVcsClient } from '../vcs';
import { maybeResolveVersionsAsync as maybeResolveAndroidVersionsAsync } from './android/version';
import { BuildContext } from './context';
import { maybeResolveVersionsAsync as maybeResolveIosVersionsAsync } from './ios/version';
@@ -23,6 +24,7 @@ import { LocalBuildMode } from './local';
export async function collectMetadataAsync<T extends Platform>(
ctx: BuildContext<T>
): Promise<Metadata> {
const vcsClient = getVcsClient();
const channelOrReleaseChannel = await resolveChannelOrReleaseChannelAsync(ctx);
const distribution =
('simulator' in ctx.buildProfile && ctx.buildProfile.simulator
@@ -42,14 +44,14 @@ export async function collectMetadataAsync<T extends Platform>(
appName: ctx.exp.name,
appIdentifier: resolveAppIdentifier(ctx),
buildProfile: ctx.buildProfileName,
gitCommitHash: await ctx.vcsClient.getCommitHashAsync(),
gitCommitHash: await vcsClient.getCommitHashAsync(),
gitCommitMessage: truncateGitCommitMessage(
(await ctx.vcsClient.getLastCommitMessageAsync()) ?? undefined
(await vcsClient.getLastCommitMessageAsync()) ?? undefined
),
isGitWorkingTreeDirty:
ctx.localBuildOptions.localBuildMode === LocalBuildMode.INTERNAL
? false
: await ctx.vcsClient.hasUncommittedChangesAsync(),
: await vcsClient.hasUncommittedChangesAsync(),
username: getUsername(ctx.exp, ctx.user),
message: ctx.message,
...(ctx.platform === Platform.IOS && {
@@ -61,10 +63,6 @@ export async function collectMetadataAsync<T extends Platform>(
runFromCI: ctx.runFromCI,
buildMode: ctx.buildProfile.config ? BuildMode.CUSTOM : BuildMode.BUILD,
customWorkflowName: ctx.customBuildConfigMetadata?.workflowName,
developmentClient: ctx.developmentClient,
requiredPackageManager: ctx.requiredPackageManager ?? undefined,
selectedImage: ctx.buildProfile.image,
customNodeVersion: ctx.buildProfile.node,
};
return sanitizeMetadata(metadata);
}
@@ -77,8 +75,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
const resolvedVersion = await maybeResolveIosVersionsAsync(
ctx.projectDir,
ctx.exp,
iosContext.ios.targets,
ctx.vcsClient
iosContext.ios.targets
);
if (iosContext.ios.buildNumberOverride) {
return {
@@ -92,8 +89,7 @@ async function maybeResolveVersionsAsync<T extends Platform>(
const resolvedVersion = await maybeResolveAndroidVersionsAsync(
ctx.projectDir,
ctx.exp,
androidCtx.buildProfile,
ctx.vcsClient
androidCtx.buildProfile
);
if (androidCtx.android.versionCodeOverride) {
return {
+12 -61
View File
@@ -1,4 +1,3 @@
import { ExpoConfig } from '@expo/config-types';
import { Platform, Workflow } from '@expo/eas-build-job';
import {
AppVersionSource,
@@ -14,7 +13,6 @@ import chalk from 'chalk';
import nullthrows from 'nullthrows';
import { Analytics } from '../analytics/AnalyticsManager';
import { createAndLinkChannelAsync, doesChannelExistAsync } from '../channel/queries';
import { DynamicConfigContextFn } from '../commandUtils/context/DynamicProjectConfigContextField';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import {
@@ -26,7 +24,7 @@ import {
} from '../graphql/generated';
import { BuildQuery } from '../graphql/queries/BuildQuery';
import { toAppPlatform, toPlatform } from '../graphql/types/AppPlatform';
import Log, { learnMore } from '../log';
import Log from '../log';
import {
RequestedPlatform,
appPlatformDisplayNames,
@@ -40,9 +38,9 @@ import {
import { checkExpoSdkIsSupportedAsync } from '../project/expoSdk';
import { validateMetroConfigForManagedWorkflowAsync } from '../project/metroConfig';
import {
installExpoUpdatesAsync,
isExpoUpdatesInstalledAsDevDependency,
isExpoUpdatesInstalledOrAvailable,
isUsingEASUpdate,
validateAppVersionRuntimePolicySupportAsync,
} from '../project/projectUtils';
import {
@@ -59,14 +57,13 @@ import {
waitToCompleteAsync as waitForSubmissionsToCompleteAsync,
} from '../submit/submit';
import { printSubmissionDetailsUrls } from '../submit/utils/urls';
import { ensureEASUpdateIsConfiguredAsync } from '../update/configure';
import { validateBuildProfileConfigMatchesProjectConfigAsync } from '../update/utils';
import { Actor } from '../user/User';
import { downloadAndMaybeExtractAppAsync } from '../utils/download';
import { truthy } from '../utils/expodash/filter';
import { printJsonOnlyOutput } from '../utils/json';
import { ProfileData, getProfilesAsync } from '../utils/profiles';
import { Client } from '../vcs/vcs';
import { getVcsClient } from '../vcs';
import { prepareAndroidBuildAsync } from './android/build';
import { BuildRequestSender, MaybeBuildFragment, waitForBuildEndAsync } from './build';
import { ensureProjectConfiguredAsync } from './configure';
@@ -98,19 +95,17 @@ export interface BuildFlags {
export async function runBuildAndSubmitAsync(
graphqlClient: ExpoGraphqlClient,
analytics: Analytics,
vcsClient: Client,
projectDir: string,
flags: BuildFlags,
actor: Actor,
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn
): Promise<void> {
await vcsClient.ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(vcsClient, flags.nonInteractive);
await getVcsClient().ensureRepoExistsAsync();
await ensureRepoIsCleanAsync(flags.nonInteractive);
await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: flags.nonInteractive,
vcsClient,
});
const easJsonAccessor = EasJsonAccessor.fromProjectPath(projectDir);
const easJsonCliConfig: EasJson['cli'] =
@@ -122,21 +117,12 @@ export async function runBuildAndSubmitAsync(
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
Log.log(
`Loaded "env" configuration for the "${buildProfiles[0].profileName}" profile: ${
buildProfiles[0].profile.env
? Object.keys(buildProfiles[0].profile.env).join(', ')
: 'no environment variables specified'
}. ${learnMore('https://docs.expo.dev/build-reference/variables/')}`
);
await ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
nonInteractive: flags.nonInteractive,
buildProfiles,
vcsClient,
});
const customBuildConfigMetadataByPlatform: { [p in AppPlatform]?: CustomBuildConfigMetadata } =
@@ -166,7 +152,6 @@ export async function runBuildAndSubmitAsync(
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata: customBuildConfigMetadataByPlatform[platform],
});
@@ -191,7 +176,6 @@ export async function runBuildAndSubmitAsync(
platforms,
profileName: flags.submitProfile,
type: 'submit',
projectDir,
});
for (const startedBuild of startedBuilds) {
const submitProfile = nullthrows(
@@ -231,6 +215,8 @@ export async function runBuildAndSubmitAsync(
const builds = await waitForBuildEndAsync(graphqlClient, {
buildIds: startedBuilds.map(({ build }) => build.id),
accountName,
projectDir,
nonInteractive: flags.nonInteractive,
});
if (!flags.json) {
printBuildResults(builds);
@@ -276,7 +262,6 @@ async function prepareAndStartBuildAsync({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
}: {
@@ -288,7 +273,6 @@ async function prepareAndStartBuildAsync({
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
customBuildConfigMetadata?: CustomBuildConfigMetadata;
}): Promise<{ build: BuildFragment | undefined; buildCtx: BuildContext<Platform> }> {
@@ -307,7 +291,6 @@ async function prepareAndStartBuildAsync({
actor,
graphqlClient,
analytics,
vcsClient,
getDynamicPrivateProjectConfigAsync,
customBuildConfigMetadata,
});
@@ -330,27 +313,11 @@ async function prepareAndStartBuildAsync({
);
if (buildProfile.profile.channel) {
await validateExpoUpdatesInstalledAsProjectDependencyAsync({
graphqlClient,
exp: buildCtx.exp,
projectId: buildCtx.projectId,
projectDir,
vcsClient: buildCtx.vcsClient,
sdkVersion: buildCtx.exp.sdkVersion,
nonInteractive: flags.nonInteractive,
buildProfile,
});
if (isUsingEASUpdate(buildCtx.exp, buildCtx.projectId)) {
const doesChannelExist = await doesChannelExistAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
});
if (!doesChannelExist) {
await createAndLinkChannelAsync(graphqlClient, {
appId: buildCtx.projectId,
channelName: buildProfile.profile.channel,
});
}
}
}
await validateAppVersionRuntimePolicySupportAsync(buildCtx.projectDir, buildCtx.exp);
@@ -417,7 +384,6 @@ async function prepareAndStartSubmissionAsync({
analytics: buildCtx.analytics,
projectId: buildCtx.projectId,
exp: buildCtx.exp,
vcsClient: buildCtx.vcsClient,
});
if (moreBuilds) {
@@ -474,20 +440,12 @@ async function maybeDownloadAndRunSimulatorBuildsAsync(
}
async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
exp,
graphqlClient,
projectId,
projectDir,
vcsClient,
buildProfile,
nonInteractive,
sdkVersion,
}: {
graphqlClient: ExpoGraphqlClient;
exp: ExpoConfig;
projectId: string;
projectDir: string;
vcsClient: Client;
buildProfile: ProfileData<'build'>;
nonInteractive: boolean;
sdkVersion?: string;
@@ -502,25 +460,18 @@ async function validateExpoUpdatesInstalledAsProjectDependencyAsync({
);
} else if (nonInteractive) {
Log.warn(
`The build profile "${buildProfile.profileName}" has specified the channel "${buildProfile.profile.channel}", but the "expo-updates" package hasn't been installed. To use channels for your builds, install the "expo-updates" package by running "npx expo install expo-updates" followed by "eas update:configure".`
`The build profile "${buildProfile.profileName}" has specified the channel "${buildProfile.profile.channel}", but the "expo-updates" package hasn't been installed. To use channels for your builds, install the "expo-updates" package by running "npx expo install expo-updates".`
);
} else {
Log.warn(
`The build profile "${buildProfile.profileName}" specifies the channel "${buildProfile.profile.channel}", but the "expo-updates" package is missing. To use channels in your builds, install the "expo-updates" package and run "eas update:configure".`
`The build profile "${buildProfile.profileName}" specifies the channel "${buildProfile.profile.channel}", but the "expo-updates" package is missing. To use channels in your builds, install the "expo-updates" package.`
);
const installExpoUpdates = await confirmAsync({
message: `Would you like to install the "expo-updates" package and configure EAS Update now?`,
message: `Would you like to install the "expo-updates" package?`,
});
if (installExpoUpdates) {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
platform: RequestedPlatform.All,
vcsClient,
});
Log.withTick('Installed expo-updates and configured EAS Update.');
throw new Error('Command must be re-run to pick up new updates configuration.');
await installExpoUpdatesAsync(projectDir, { silent: false });
Log.withTick('Installed expo-updates');
}
}
}
@@ -10,17 +10,15 @@ import { resolveWorkflowAsync } from '../../project/workflow';
import { confirmAsync } from '../../prompts';
import { expoCommandAsync } from '../../utils/expoCli';
import { ProfileData } from '../../utils/profiles';
import { Client } from '../../vcs/vcs';
import { getVcsClient } from '../../vcs';
import { reviewAndCommitChangesAsync } from './repository';
export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
projectDir,
vcsClient,
nonInteractive = false,
buildProfiles = [],
}: {
projectDir: string;
vcsClient: Client;
nonInteractive?: boolean;
buildProfiles?: ProfileData<'build'>[];
}): Promise<void> {
@@ -43,7 +41,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
);
const workflowPerPlatformList = await Promise.all(
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform, vcsClient))
platformsToCheck.map(platform => resolveWorkflowAsync(projectDir, platform))
);
Log.newLine();
@@ -64,7 +62,7 @@ export async function ensureExpoDevClientInstalledForDevClientBuildsAsync({
instructions: 'The command will abort unless you agree.',
});
if (install) {
await installExpoDevClientAsync(projectDir, vcsClient, { nonInteractive });
await installExpoDevClientAsync(projectDir, { nonInteractive });
} else {
Errors.error(`Install ${chalk.bold('expo-dev-client')} manually and come back later.`, {
exit: 1,
@@ -102,7 +100,6 @@ async function isExpoDevClientInstalledAsync(projectDir: string): Promise<boolea
async function installExpoDevClientAsync(
projectDir: string,
vcsClient: Client,
{ nonInteractive }: { nonInteractive: boolean }
): Promise<void> {
Log.newLine();
@@ -110,8 +107,8 @@ async function installExpoDevClientAsync(
Log.newLine();
await expoCommandAsync(projectDir, ['install', 'expo-dev-client']);
Log.newLine();
if (await vcsClient.isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync(vcsClient, 'Install expo-dev-client', {
if (await getVcsClient().isCommitRequiredAsync()) {
await reviewAndCommitChangesAsync('Install expo-dev-client', {
nonInteractive,
});
}
+20 -29
View File
@@ -10,10 +10,10 @@ import { confirmAsync, promptAsync } from '../../prompts';
import { formatBytes } from '../../utils/files';
import { getTmpDirectory } from '../../utils/paths';
import { endTimer, formatMilliseconds, startTimer } from '../../utils/timer';
import { Client } from '../../vcs/vcs';
import { getVcsClient } from '../../vcs';
export async function maybeBailOnRepoStatusAsync(vcsClient: Client): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) {
export async function maybeBailOnRepoStatusAsync(): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -32,11 +32,8 @@ export async function maybeBailOnRepoStatusAsync(vcsClient: Client): Promise<voi
}
}
export async function ensureRepoIsCleanAsync(
vcsClient: Client,
nonInteractive = false
): Promise<void> {
if (!(await vcsClient.isCommitRequiredAsync())) {
export async function ensureRepoIsCleanAsync(nonInteractive = false): Promise<void> {
if (!(await getVcsClient().isCommitRequiredAsync())) {
return;
}
Log.addNewLineIfNone();
@@ -53,22 +50,19 @@ export async function ensureRepoIsCleanAsync(
message: `Commit changes to git?`,
});
if (answer) {
await commitPromptAsync(vcsClient, { commitAllFiles: true });
await commitPromptAsync({ commitAllFiles: true });
} else {
throw new Error('Commit all changes. Aborting...');
}
}
export async function commitPromptAsync(
vcsClient: Client,
{
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}
): Promise<void> {
export async function commitPromptAsync({
initialCommitMessage,
commitAllFiles,
}: {
initialCommitMessage?: string;
commitAllFiles?: boolean;
} = {}): Promise<void> {
const { message } = await promptAsync({
type: 'text',
name: 'message',
@@ -76,16 +70,14 @@ export async function commitPromptAsync(
initial: initialCommitMessage,
validate: (input: string) => input !== '',
});
await vcsClient.commitAsync({
await getVcsClient().commitAsync({
commitAllFiles,
commitMessage: message,
nonInteractive: false,
});
}
export async function makeProjectTarballAsync(
vcsClient: Client
): Promise<{ path: string; size: number }> {
export async function makeProjectTarballAsync(): Promise<{ path: string; size: number }> {
const spinner = ora('Compressing project files');
await fs.mkdirp(getTmpDirectory());
@@ -107,7 +99,7 @@ export async function makeProjectTarballAsync(
startTimer(compressTimerLabel);
try {
await vcsClient.makeShallowCopyAsync(shallowClonePath);
await getVcsClient().makeShallowCopyAsync(shallowClonePath);
await tar.create({ cwd: shallowClonePath, file: tarPath, prefix: 'project', gzip: true }, [
'.',
]);
@@ -141,12 +133,11 @@ enum ShouldCommitChanges {
}
export async function reviewAndCommitChangesAsync(
vcsClient: Client,
initialCommitMessage: string,
{ nonInteractive, askedFirstTime = true }: { nonInteractive: boolean; askedFirstTime?: boolean }
): Promise<void> {
if (process.env.EAS_BUILD_AUTOCOMMIT) {
await vcsClient.commitAsync({
await getVcsClient().commitAsync({
commitMessage: initialCommitMessage,
commitAllFiles: false,
nonInteractive,
@@ -180,11 +171,11 @@ export async function reviewAndCommitChangesAsync(
"Aborting, run the command again once you're ready. Make sure to commit any changes you've made."
);
} else if (selected === ShouldCommitChanges.Yes) {
await commitPromptAsync(vcsClient, { initialCommitMessage });
await commitPromptAsync({ initialCommitMessage });
Log.withTick('Committed changes.');
} else if (selected === ShouldCommitChanges.ShowDiffFirst) {
await vcsClient.showDiffAsync();
await reviewAndCommitChangesAsync(vcsClient, initialCommitMessage, {
await getVcsClient().showDiffAsync();
await reviewAndCommitChangesAsync(initialCommitMessage, {
nonInteractive,
askedFirstTime: false,
});
@@ -7,13 +7,21 @@ import Log, { link } from '../../log';
type AndroidResourceClass = Exclude<
ResourceClass,
ResourceClass.M1_MEDIUM | ResourceClass.M_MEDIUM | ResourceClass.M_LARGE
| ResourceClass.M1_EXPERIMENTAL
| ResourceClass.M1_MEDIUM
| ResourceClass.M1_LARGE
| ResourceClass.INTEL_MEDIUM
| ResourceClass.M_MEDIUM
| ResourceClass.M_LARGE
>;
const iosResourceClassToBuildResourceClassMapping: Record<ResourceClass, BuildResourceClass> = {
[ResourceClass.DEFAULT]: BuildResourceClass.IosDefault,
[ResourceClass.LARGE]: BuildResourceClass.IosLarge,
[ResourceClass.M1_EXPERIMENTAL]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M1_LARGE]: BuildResourceClass.IosMLarge,
[ResourceClass.INTEL_MEDIUM]: BuildResourceClass.IosIntelMedium,
[ResourceClass.MEDIUM]: BuildResourceClass.IosMedium,
[ResourceClass.M_MEDIUM]: BuildResourceClass.IosMMedium,
[ResourceClass.M_LARGE]: BuildResourceClass.IosMLarge,
@@ -49,7 +57,15 @@ export async function resolveBuildResourceClassAsync<T extends Platform>(
}
function resolveAndroidResourceClass(selectedResourceClass?: ResourceClass): BuildResourceClass {
if (selectedResourceClass && ResourceClass.M1_MEDIUM === selectedResourceClass) {
if (
selectedResourceClass &&
[
ResourceClass.M1_EXPERIMENTAL,
ResourceClass.M1_MEDIUM,
ResourceClass.M1_LARGE,
ResourceClass.INTEL_MEDIUM,
].includes(selectedResourceClass)
) {
throw new Error(`Resource class ${selectedResourceClass} is only available for iOS builds`);
}
@@ -72,7 +88,7 @@ function resolveIosResourceClass(
);
}
if (ResourceClass.M1_MEDIUM === resourceClass) {
if ([ResourceClass.M1_EXPERIMENTAL, ResourceClass.M1_MEDIUM].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'm-medium'
@@ -80,7 +96,7 @@ function resolveIosResourceClass(
);
}
if (ResourceClass.M_LARGE === resourceClass) {
if ([ResourceClass.M_LARGE, ResourceClass.M1_LARGE].includes(resourceClass)) {
Log.warn(
`Resource class ${chalk.bold(resourceClass)} is deprecated. Use ${chalk.bold(
'large'
+3 -2
View File
@@ -6,6 +6,7 @@ import semver from 'semver';
import Log, { learnMore } from '../log';
import { isPNGAsync } from '../utils/image';
import { getVcsClient } from '../vcs';
import { CommonContext } from './context';
export function checkNodeEnvVariable(ctx: CommonContext<Platform>): void {
@@ -27,12 +28,12 @@ export async function checkGoogleServicesFileAsync<T extends Platform>(
if (!googleServicesFilePath) {
return;
}
const rootDir = path.normalize(await ctx.vcsClient.getRootPathAsync());
const rootDir = path.normalize(await getVcsClient().getRootPathAsync());
const absGoogleServicesFilePath = path.resolve(ctx.projectDir, googleServicesFilePath);
if (
(await fs.pathExists(absGoogleServicesFilePath)) &&
(!isInsideDirectory(absGoogleServicesFilePath, rootDir) ||
(await ctx.vcsClient.isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
(await getVcsClient().isFileIgnoredAsync(path.relative(rootDir, absGoogleServicesFilePath))))
) {
Log.warn(
`File specified via "${ctx.platform}.googleServicesFile" field in your app.json is not checked in to your repository and won't be uploaded to the builder.`
@@ -1,22 +0,0 @@
import { BranchBasicInfo, ChannelBasicInfo, UpdateChannelInfoWithBranches } from '../utils';
export const testChannelBasicInfo: ChannelBasicInfo = {
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
name: 'production',
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
};
export const testBasicBranchInfo1: BranchBasicInfo = {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
};
export const testBasicBranchInfo2: BranchBasicInfo = {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
};
export const channelInfoWithBranches: UpdateChannelInfoWithBranches<BranchBasicInfo> = {
...testChannelBasicInfo,
updateBranches: [testBasicBranchInfo1, testBasicBranchInfo2],
};
@@ -1,70 +0,0 @@
import {
emptyBranchMapping,
rolloutBranchMapping,
standardBranchMapping,
} from '../../rollout/__tests__/fixtures';
import {
BranchMappingValidationError,
assertVersion,
getAlwaysTrueBranchMapping,
getBranchMapping,
getEmptyBranchMapping,
getStandardBranchId,
isAlwaysTrueBranchMapping,
isEmptyBranchMapping,
} from '../branch-mapping';
import { testChannelBasicInfo } from './branch-mapping-fixtures';
describe(assertVersion, () => {
it('throws if the branch mapping is not the correct version', () => {
expect(() => assertVersion(testChannelBasicInfo, 5)).toThrowError(BranchMappingValidationError);
});
it('asserts the correct version', () => {
assertVersion(testChannelBasicInfo, 0);
expect(getBranchMapping(testChannelBasicInfo.branchMapping).version).toBe(0);
});
});
describe(getEmptyBranchMapping, () => {
it('gets an empty branch mapping', () => {
expect(isEmptyBranchMapping(getEmptyBranchMapping())).toBe(true);
});
});
describe(isEmptyBranchMapping, () => {
it('detects empty branch mappings', () => {
expect(isEmptyBranchMapping(emptyBranchMapping)).toBe(true);
expect(isEmptyBranchMapping(standardBranchMapping)).toBe(false);
expect(isEmptyBranchMapping(rolloutBranchMapping)).toBe(false);
});
});
describe(isAlwaysTrueBranchMapping, () => {
it('detects always true branch mappings', () => {
expect(isAlwaysTrueBranchMapping(standardBranchMapping)).toBe(true);
expect(isAlwaysTrueBranchMapping(rolloutBranchMapping)).toBe(false);
expect(isAlwaysTrueBranchMapping(emptyBranchMapping)).toBe(false);
});
});
describe(getAlwaysTrueBranchMapping, () => {
it('gets an always true branch mapping', () => {
const alwaysTrueBranchMapping = getAlwaysTrueBranchMapping('test-id');
expect(isAlwaysTrueBranchMapping(alwaysTrueBranchMapping)).toBe(true);
});
});
describe(getStandardBranchId, () => {
it('throws if the branch mapping is not a standard mapping', () => {
expect(() => getStandardBranchId(testChannelBasicInfo)).toThrowError(
BranchMappingValidationError
);
});
it('gets a standard branch id', () => {
const channelObjectWithStandardMapping = { ...testChannelBasicInfo };
channelObjectWithStandardMapping.branchMapping = JSON.stringify(standardBranchMapping);
expect(getStandardBranchId(channelObjectWithStandardMapping)).toBe(
standardBranchMapping.data[0].branchId
);
});
});
@@ -1,148 +0,0 @@
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
import { UpdateBranchObject, UpdateChannelObject } from '../../graphql/queries/ChannelQuery';
export const testUpdateBranch1: UpdateBranchObject = {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
updateGroups: [
[
{
id: 'bbe3e63a-f620-4603-8c4d-258f3e51590d',
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
message: 'fix bug',
createdAt: '2023-07-17T22:48:59.278Z',
runtimeVersion: 'exposdk:48.0.0',
platform: 'ios',
manifestFragment:
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"7d40544b395c5949f4646f5e150fe020","fileSHA256":"LN_rjlzN55dvcBL7jM5zryIpAp7iHU8VCf7RFIccbNg","storageKey":"Pi65ns2w9xLI5G_m9aGrJ5wK5FpWcGcInSSFwnA_0aA","contentType":"image/png","fileExtension":".png"},{"bundleKey":"cdd04e13d4ec83ff0cd13ec8dabdc341","fileSHA256":"irHji01LPI9giDYxtJo9bTPHaEHzuUcCMTlb4xn90Ks","storageKey":"oJZ9JX1m8IKNDgnIqcKOo5KUFOoa_NCaxruigZu65E0","contentType":"image/png","fileExtension":".png"},{"bundleKey":"a132ecc4ba5c1517ff83c0fb321bc7fc","fileSHA256":"K3RDqaWOksoRpXW9wRWYYVpX3eMFzYznDWAmBrMCzZg","storageKey":"X3OAkUu5P_LqyKFx1bNkA_qDQFLlSjCYNAlVEaOamHE","contentType":"image/png","fileExtension":".png"},{"bundleKey":"0ea69b5077e7c4696db85dbcba75b0e1","fileSHA256":"Pd0Hc-0n4j0leJ8wFzGzrEVa2L5aonoKG4OPicbSciU","storageKey":"FwJkAbrRNahBGX1KXnTAgiiwyzENu3D7DeEzCwmInyo","contentType":"image/png","fileExtension":".png"},{"bundleKey":"f5b790e2ac193b3d41015edb3551f9b8","fileSHA256":"vCfIss7SLYKt1AAi0hxjsgOMEDceLJB6ZWjQf6gTU4k","storageKey":"GQtYm7r0G9N3WJyrymjZ26p8eNMIb7rzqwSVgGco5EU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"3d0ac172fe41ab941a8c68cd7e3a17ee","fileSHA256":"jjnar-ZPrb_g0Bx5tcFfjJrP3v8HdeAlXMk__xN7I1U","storageKey":"bimf-UXq5IFH5nNHCIHdcQ2MyJwTnwDVBb-P8hr2CZw","contentType":"application/javascript","fileExtension":".bundle"}}',
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/bbe3e63a-f620-4603-8c4d-258f3e51590d',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
username: 'quintest113',
},
branch: {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
__typename: 'UpdateBranch',
},
codeSigningInfo: null,
__typename: 'Update',
},
{
id: 'db22401b-d321-4629-a068-21d6d1f19c02',
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
message: 'fix bug',
createdAt: '2023-07-17T22:48:59.278Z',
runtimeVersion: 'exposdk:48.0.0',
platform: 'android',
manifestFragment:
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"778ffc9fe8773a878e9c30a6304784de","fileSHA256":"i2Gkx-9w3JJ1PwSUl2SC9m_UFQ7CPfx3KrZeEDc6-lU","storageKey":"idnX8z03q4vGLzhYMPaQGBADjhqvWe67_afPqJ1vGzM","contentType":"image/png","fileExtension":".png"},{"bundleKey":"376d6a4c7f622917c39feb23671ef71d","fileSHA256":"QJsG0VpGM-5viirUgoKq0M4zQ_TYyPfLhAhN5mrM54I","storageKey":"46DQ95-UdSKiRkQgt5FyXkE1FrKOT9tBZP3nS7auKlg","contentType":"image/png","fileExtension":".png"},{"bundleKey":"c79c3606a1cf168006ad3979763c7e0c","fileSHA256":"kGZm3WiTRq2iMs42ia3vkHtCV_obWT8DY40rlJf2SIQ","storageKey":"k-3xHp3vP8mR36WdccUZVPTRJKpP-zlyboWdkXCIQpQ","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02bc1fa7c0313217bde2d65ccbff40c9","fileSHA256":"_6fuRbdkBbpzkhSVAI99aMneY5X0tpQdsGNGX244IyA","storageKey":"lfqeUjiWFXwowhxVDBEzxsbTOEzYxMLebljy5Bh1mpU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"35ba0eaec5a4f5ed12ca16fabeae451d","fileSHA256":"hM9es7ICUPaeDkczvrTaX0F_BoKJKlrRteYlcHU8IbE","storageKey":"fMoMrsUeB5xWw_Ugn8FqGJf-M2pld2hkXKR704z0MM8","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"4cc2ee49988e438ac066827b809721de","fileSHA256":"ctfK72f8EdS1IQmDvJLya38RMSma16beJcMoQToZIe0","storageKey":"QdEoKuCQPvVPA71gY52wqJ9vmtV4ObnCcPE2FP4SIdo","contentType":"application/javascript","fileExtension":".bundle"}}',
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/db22401b-d321-4629-a068-21d6d1f19c02',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
username: 'quintest113',
},
branch: {
id: '754bf17f-efc0-46ab-8a59-a03f20e53e9b',
name: 'wrong-channel',
__typename: 'UpdateBranch',
},
codeSigningInfo: null,
__typename: 'Update',
},
],
],
__typename: 'UpdateBranch',
};
export const testUpdateBranch2: UpdateBranchObject = {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
updateGroups: [
[
{
id: 'f04aafc1-0bf5-4548-8c35-726b467e7c40',
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
message: 'fix bug',
createdAt: '2023-06-23T23:37:10.004Z',
runtimeVersion: 'exposdk:48.0.0',
platform: 'ios',
manifestFragment:
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"7d40544b395c5949f4646f5e150fe020","fileSHA256":"LN_rjlzN55dvcBL7jM5zryIpAp7iHU8VCf7RFIccbNg","storageKey":"Pi65ns2w9xLI5G_m9aGrJ5wK5FpWcGcInSSFwnA_0aA","contentType":"image/png","fileExtension":".png"},{"bundleKey":"cdd04e13d4ec83ff0cd13ec8dabdc341","fileSHA256":"irHji01LPI9giDYxtJo9bTPHaEHzuUcCMTlb4xn90Ks","storageKey":"oJZ9JX1m8IKNDgnIqcKOo5KUFOoa_NCaxruigZu65E0","contentType":"image/png","fileExtension":".png"},{"bundleKey":"a132ecc4ba5c1517ff83c0fb321bc7fc","fileSHA256":"K3RDqaWOksoRpXW9wRWYYVpX3eMFzYznDWAmBrMCzZg","storageKey":"X3OAkUu5P_LqyKFx1bNkA_qDQFLlSjCYNAlVEaOamHE","contentType":"image/png","fileExtension":".png"},{"bundleKey":"0ea69b5077e7c4696db85dbcba75b0e1","fileSHA256":"Pd0Hc-0n4j0leJ8wFzGzrEVa2L5aonoKG4OPicbSciU","storageKey":"FwJkAbrRNahBGX1KXnTAgiiwyzENu3D7DeEzCwmInyo","contentType":"image/png","fileExtension":".png"},{"bundleKey":"f5b790e2ac193b3d41015edb3551f9b8","fileSHA256":"vCfIss7SLYKt1AAi0hxjsgOMEDceLJB6ZWjQf6gTU4k","storageKey":"GQtYm7r0G9N3WJyrymjZ26p8eNMIb7rzqwSVgGco5EU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"76ec49faddaef30ba78fe50edc5ab3d6","fileSHA256":"OLUdgP-fZI2shwGS5qlIk7nWnybL_13RJl5xR85CDBU","storageKey":"OCvI0-34zApbdo3eSGfIaQUbWdZeLeKH-nJSeedKTFs","contentType":"application/javascript","fileExtension":".bundle"}}',
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/f04aafc1-0bf5-4548-8c35-726b467e7c40',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
username: 'quintest113',
},
branch: {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
__typename: 'UpdateBranch',
},
codeSigningInfo: null,
__typename: 'Update',
},
{
id: 'd8d15786-2727-407f-bf3e-f4bda7e397d9',
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
message: 'fix bug',
createdAt: '2023-06-23T23:37:10.004Z',
runtimeVersion: 'exposdk:48.0.0',
platform: 'android',
manifestFragment:
'{"extra":{"expoClient":{"ios":{"supportsTablet":true,"bundleIdentifier":"com.quintest113.updatestest4"},"web":{"favicon":"./assets/favicon.png"},"icon":"./assets/icon.png","name":"updates-test-4","slug":"updates-test-4","extra":{"eas":{"projectId":"789a8784-0186-4f41-8bd2-7f219291a2b8"}},"splash":{"image":"./assets/splash.png","resizeMode":"contain","backgroundColor":"#ffffff"},"android":{"package":"com.quintest113.updatestest4","adaptiveIcon":{"backgroundColor":"#ffffff","foregroundImage":"./assets/adaptive-icon.png"}},"updates":{"url":"https://u.expo.dev/789a8784-0186-4f41-8bd2-7f219291a2b8"},"version":"1.0.0","platforms":["ios","android"],"sdkVersion":"48.0.0","orientation":"portrait","runtimeVersion":{"policy":"sdkVersion"},"currentFullName":"@quintest113/updates-test-4","originalFullName":"@quintest113/updates-test-4","userInterfaceStyle":"light","assetBundlePatterns":["**/*"]}},"assets":[{"bundleKey":"778ffc9fe8773a878e9c30a6304784de","fileSHA256":"i2Gkx-9w3JJ1PwSUl2SC9m_UFQ7CPfx3KrZeEDc6-lU","storageKey":"idnX8z03q4vGLzhYMPaQGBADjhqvWe67_afPqJ1vGzM","contentType":"image/png","fileExtension":".png"},{"bundleKey":"376d6a4c7f622917c39feb23671ef71d","fileSHA256":"QJsG0VpGM-5viirUgoKq0M4zQ_TYyPfLhAhN5mrM54I","storageKey":"46DQ95-UdSKiRkQgt5FyXkE1FrKOT9tBZP3nS7auKlg","contentType":"image/png","fileExtension":".png"},{"bundleKey":"c79c3606a1cf168006ad3979763c7e0c","fileSHA256":"kGZm3WiTRq2iMs42ia3vkHtCV_obWT8DY40rlJf2SIQ","storageKey":"k-3xHp3vP8mR36WdccUZVPTRJKpP-zlyboWdkXCIQpQ","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02bc1fa7c0313217bde2d65ccbff40c9","fileSHA256":"_6fuRbdkBbpzkhSVAI99aMneY5X0tpQdsGNGX244IyA","storageKey":"lfqeUjiWFXwowhxVDBEzxsbTOEzYxMLebljy5Bh1mpU","contentType":"image/png","fileExtension":".png"},{"bundleKey":"35ba0eaec5a4f5ed12ca16fabeae451d","fileSHA256":"hM9es7ICUPaeDkczvrTaX0F_BoKJKlrRteYlcHU8IbE","storageKey":"fMoMrsUeB5xWw_Ugn8FqGJf-M2pld2hkXKR704z0MM8","contentType":"image/png","fileExtension":".png"},{"bundleKey":"5223c8d9b0d08b82a5670fb5f71faf78","fileSHA256":"LJXowmdeeZM1_QBGuwfb2gbLfs_1HtN5IsYwnWGcxNM","storageKey":"jeqOrdQBNY4hgl04B3wtGs8QNWMDmjvbjIMUXPtOBpw","contentType":"image/png","fileExtension":".png"},{"bundleKey":"02d5848f62ade9617cf96894727d32e7","fileSHA256":"u1-8zHOj9oxS8maH-7JbShJIq1-KEV7FXnrGpEUcR-4","storageKey":"LMUvUZZpdGFb7KqaB_l9N4TXTkFn1Pw92ws9fWDNZPw","contentType":"model/gltf-binary","fileExtension":".glb"},{"bundleKey":"6013c27e421ab23c826713af3ebec891","fileSHA256":"dutQNnvzTCS4NgwvbeMbvhro5QnW0GJUeY_4rrvlcLY","storageKey":"85KB9Jr7eqz1Jwt1mYQGRzTy6ZXyO0j7ZcaLImCbzkU","contentType":"image/jpeg","fileExtension":".jpg"}],"launchAsset":{"bundleKey":"f4c6981626dbabcf51078d07a1611f4e","fileSHA256":"q-k1hSWl49aXpSg4jOSaVVnYFSFL6xf7HVl_TQE-ObM","storageKey":"FwK1PaZqtpL0QZIgaHsxU1AV93JhMdu9K1wZizXROAc","contentType":"application/javascript","fileExtension":".bundle"}}',
isRollBackToEmbedded: false,
manifestPermalink: 'https://u.expo.dev/update/d8d15786-2727-407f-bf3e-f4bda7e397d9',
gitCommitHash: 'e91c5bb733eff675dd8a3722ed5d4f885a4b10ce',
actor: {
__typename: 'User',
id: '026ace2b-c1f8-4a02-818d-6b52c100bdf2',
username: 'quintest113',
},
branch: {
id: '6941a8dd-5c0a-48bc-8876-f49c88ed419f',
name: 'production',
__typename: 'UpdateBranch',
},
codeSigningInfo: null,
__typename: 'Update',
},
],
],
__typename: 'UpdateBranch',
};
export const testChannelObject: UpdateChannelObject = {
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
name: 'production',
createdAt: '2023-05-16T21:26:16.946Z',
updatedAt: '2023-05-16T21:26:16.946Z',
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.15,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
updateBranches: [testUpdateBranch1, testUpdateBranch2],
__typename: 'UpdateChannel',
};
export const testBasicChannelInfo: UpdateChannelBasicInfoFragment = {
id: '9309afc2-9752-40db-8ef7-4abc10744c61',
name: 'production',
branchMapping:
'{"data":[{"branchId":"754bf17f-efc0-46ab-8a59-a03f20e53e9b","branchMappingLogic":{"operand":0.1,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"6941a8dd-5c0a-48bc-8876-f49c88ed419f","branchMappingLogic":"true"}],"version":0}',
__typename: 'UpdateChannel',
};
export const testBasicChannelInfo2: UpdateChannelBasicInfoFragment = {
id: '9d5c7bf0-d52d-474c-9140-c1bad7f0de9d',
name: 'staging',
branchMapping:
'{"data":[{"branchId":"d7d68e32-d9c9-4a8d-8d1b-21e53100a5e8","branchMappingLogic":{"operand":0.1,"clientKey":"rolloutToken","branchMappingOperator":"hash_lt"}},{"branchId":"f9f708c2-0c91-4360-b2a4-0b61834aef4a","branchMappingLogic":"true"}],"version":0}',
__typename: 'UpdateChannel',
};
@@ -1,57 +0,0 @@
import { format } from '@expo/timeago.js';
import { getAlwaysTrueBranchMapping, getEmptyBranchMapping } from '../branch-mapping';
import { getDescriptionByBranchId } from '../print-utils';
import { testChannelObject, testUpdateBranch1, testUpdateBranch2 } from './fixtures';
describe(getDescriptionByBranchId, () => {
it('should get descriptions for multiple branches with multiple update groups', () => {
const descriptionByBranchId = getDescriptionByBranchId(testChannelObject);
expect(Object.values(descriptionByBranchId)).toHaveLength(2);
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({
branch: 'wrong-channel',
branchRolloutPercentage: 15,
update: {
codeSigningKey: undefined,
group: '16ca6dba-e63b-48b0-baa3-15a894ee9434',
isRollBackToEmbedded: false,
message: `"fix bug" (${format('2023-07-17T22:48:59.278Z', 'en_US')} by quintest113)`,
platforms: 'android, ios',
runtimeVersion: 'exposdk:48.0.0',
},
});
expect(descriptionByBranchId[testUpdateBranch2.id]).toEqual({
branch: 'production',
branchRolloutPercentage: 85,
update: {
codeSigningKey: undefined,
group: 'e40ad156-e9af-4cc2-8e9d-c7b5c328db48',
isRollBackToEmbedded: false,
message: `"fix bug" (${format('2023-06-23T23:37:10.004Z', 'en_US')} by quintest113)`,
platforms: 'android, ios',
runtimeVersion: 'exposdk:48.0.0',
},
});
});
it('should get descriptions for branches with no updates', () => {
const noUpdatesBranch = { ...testUpdateBranch1, updateGroups: [] };
const oneBranchChannel = {
...testChannelObject,
updateBranches: [noUpdatesBranch],
branchMapping: JSON.stringify(getAlwaysTrueBranchMapping(noUpdatesBranch.id)),
};
const descriptionByBranchId = getDescriptionByBranchId(oneBranchChannel);
expect(Object.values(descriptionByBranchId)).toHaveLength(1);
expect(descriptionByBranchId[testUpdateBranch1.id]).toEqual({ branch: 'wrong-channel' });
});
it('should get descriptions for no branches', () => {
const noBranchChannel = {
...testChannelObject,
updateBranches: [],
branchMapping: JSON.stringify(getEmptyBranchMapping()),
};
const descriptionByBranchId = getDescriptionByBranchId(noBranchChannel);
expect(Object.values(descriptionByBranchId)).toHaveLength(0);
});
});
@@ -1,11 +0,0 @@
import { getUpdateBranch } from '../utils';
import { channelInfoWithBranches, testBasicBranchInfo1 } from './branch-mapping-fixtures';
describe(getUpdateBranch, () => {
it('gets the update branch', () => {
expect(getUpdateBranch(channelInfoWithBranches, '754bf17f-efc0-46ab-8a59-a03f20e53e9b')).toBe(
testBasicBranchInfo1
);
expect(() => getUpdateBranch(channelInfoWithBranches, 'not-a-branch-ID')).toThrow();
});
});
@@ -1,107 +0,0 @@
import { EASUpdateAction, EASUpdateContext, NonInteractiveError } from '../../eas-update/utils';
import { UpdateChannelBasicInfoFragment } from '../../graphql/generated';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import {
Connection,
Edge,
FilterPagination,
QueryParams,
selectPaginatedAsync,
} from '../../utils/relay';
/**
* Select a channel for the project.
*
* @constructor
* @param {function} options.filterPredicate - A predicate to filter the channels that are shown to the user. It takes a channelInfo object as a parameter and returns a boolean.
* @param {string} options.printedType - The type of channel printed to the user. Defaults to 'channel'.
* @param {number} options.pageSize - The number of channels to show per page. Defaults to 100.
* @param {function} options.beforeEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called before each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @param {function} options.afterEachFilterQuery Optional. If a filter predicate was specified, this callback function will be called after each query.
* @args externalQueryParams The query params for the pagination.
* @args totalNodesFetched The total number of nodes fetched so far.
* @args dataset The dataset so far.
* @args willFetchAgain If the query will fetch again to get a complete page.
*/
export class SelectChannel implements EASUpdateAction<UpdateChannelBasicInfoFragment | null> {
constructor(
private options: {
filterPredicate?: (channelInfo: UpdateChannelBasicInfoFragment) => boolean;
printedType?: string;
pageSize?: number;
beforeEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateChannelBasicInfoFragment>[]
) => void;
afterEachFilterQuery?: (
externalQueryParams: QueryParams,
totalNodesFetched: number,
dataset: Edge<UpdateChannelBasicInfoFragment>[],
willFetchAgain: boolean
) => void;
} = {}
) {}
async queryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams
): Promise<Connection<UpdateChannelBasicInfoFragment>> {
const { graphqlClient, app } = ctx;
const { projectId } = app;
return await ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync(graphqlClient, {
appId: projectId,
...queryParams,
});
}
async filterQueryAsync(
ctx: EASUpdateContext,
queryParams: QueryParams,
filterPredicate: (channelInfo: UpdateChannelBasicInfoFragment) => boolean
): Promise<Connection<UpdateChannelBasicInfoFragment>> {
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
await this.queryAsync(ctx, queryParams);
return await FilterPagination.getPageAsync({
queryParams,
queryAsync,
filterPredicate,
beforeEachQuery: this.options.beforeEachFilterQuery,
afterEachQuery: this.options.afterEachFilterQuery,
});
}
public async runAsync(ctx: EASUpdateContext): Promise<UpdateChannelBasicInfoFragment | null> {
const { nonInteractive } = ctx;
const { filterPredicate } = this.options;
const printedType = this.options.printedType ?? 'channel';
const pageSize = this.options.pageSize ?? 100;
if (nonInteractive) {
throw new NonInteractiveError(
`${printedType} selection cannot be run in non-interactive mode.`
);
}
const queryAsync = async (
queryParams: QueryParams
): Promise<Connection<UpdateChannelBasicInfoFragment>> =>
filterPredicate
? this.filterQueryAsync(ctx, queryParams, filterPredicate)
: this.queryAsync(ctx, queryParams);
const getTitleAsync = async (channelInfo: UpdateChannelBasicInfoFragment): Promise<string> =>
channelInfo.name;
return await selectPaginatedAsync({
queryAsync,
getTitleAsync,
printedType,
pageSize,
});
}
}
@@ -1,92 +0,0 @@
import { createCtxMock } from '../../../eas-update/__tests__/fixtures';
import { NonInteractiveError } from '../../../eas-update/utils';
import { UpdateChannelBasicInfoFragment } from '../../../graphql/generated';
import { ChannelQuery } from '../../../graphql/queries/ChannelQuery';
import { promptAsync } from '../../../prompts';
import { testBasicChannelInfo, testBasicChannelInfo2 } from '../../__tests__/fixtures';
import { SelectChannel } from '../SelectChannel';
jest.mock('../../../prompts');
jest.mock('../../../graphql/queries/ChannelQuery');
describe(SelectChannel, () => {
afterEach(() => {
jest.clearAllMocks();
});
it('returns null if project has no channels', async () => {
const ctx = createCtxMock();
jest.mocked(ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const selectChannel = new SelectChannel();
const channel = await selectChannel.runAsync(ctx);
expect(channel).toBeNull();
});
it('still prompts if project has only one channel', async () => {
const ctx = createCtxMock();
jest.mocked(ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [{ node: testBasicChannelInfo, cursor: 'cursor' }],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicChannelInfo,
}));
const selectChannel = new SelectChannel();
const channel = await selectChannel.runAsync(ctx);
expect(channel).toBe(testBasicChannelInfo);
expect(promptAsync).toHaveBeenCalledTimes(1);
});
it('returns the selected channel', async () => {
jest.mocked(ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicChannelInfo, cursor: 'cursor' },
{ node: testBasicChannelInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
jest.mocked(promptAsync).mockImplementation(async () => ({
item: testBasicChannelInfo,
}));
const ctx = createCtxMock();
const selectChannel = new SelectChannel();
const channel = await selectChannel.runAsync(ctx);
expect(channel).toBe(testBasicChannelInfo);
});
it('returns the only available channel with a filter', async () => {
jest.mocked(ChannelQuery.viewUpdateChannelsBasicInfoPaginatedOnAppAsync).mockResolvedValue({
edges: [
{ node: testBasicChannelInfo, cursor: 'cursor' },
{ node: testBasicChannelInfo2, cursor: 'cursor' },
],
pageInfo: {
hasPreviousPage: false,
hasNextPage: false,
},
});
const ctx = createCtxMock();
const selectChannel = new SelectChannel({
filterPredicate: (channelInfo: UpdateChannelBasicInfoFragment) =>
channelInfo === testBasicChannelInfo,
});
const channel = await selectChannel.runAsync(ctx);
expect(channel).toBe(testBasicChannelInfo);
});
it('throws an error in Non-Interactive Mode', async () => {
const ctx = createCtxMock({
nonInteractive: true,
});
const selectChannel = new SelectChannel();
// dont fail if users are running in non-interactive mode
await expect(selectChannel.runAsync(ctx)).rejects.toThrowError(NonInteractiveError);
});
});
@@ -1,214 +0,0 @@
import { ChannelBasicInfo } from './utils';
// TODO(quin): move this into a common package with www
export type BranchMappingOperator =
| '=='
| '!='
| '<'
| '>'
| '<='
| '>='
| 'in'
| 'regex'
| 'hash_lt'
| 'hash_lte'
| 'hash_gt'
| 'hash_gte';
export type BranchMappingObject = {
clientKey: string;
branchMappingOperator: BranchMappingOperator;
operand: number | string | string[];
};
export type BranchMappingAlwaysTrue = 'true';
export type BranchMappingStatement =
| ['and' | 'or', ...BranchMappingNode[]]
| ['not', BranchMappingNode];
export type BranchMappingNode =
| BranchMappingAlwaysTrue
| BranchMappingObject
| BranchMappingStatement;
export type BranchMapping = {
version: number;
data: {
branchId: string;
branchMappingLogic: BranchMappingNode;
}[];
};
export type AlwaysTrueBranchMapping = {
version: number;
data: [
{
branchId: string;
branchMappingLogic: BranchMappingAlwaysTrue;
}
];
};
export type EmptyBranchMapping = {
version: number;
data: [];
};
export function getEmptyBranchMapping(): EmptyBranchMapping {
return {
version: 0,
data: [],
};
}
export function getAlwaysTrueBranchMapping(branchId: string): AlwaysTrueBranchMapping {
return {
version: 0,
data: [
{
branchId,
branchMappingLogic: 'true',
},
],
};
}
export function hasEmptyBranchMap(channelInfo: ChannelBasicInfo): boolean {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
return isEmptyBranchMapping(branchMapping);
}
export function hasStandardBranchMap(channelInfo: ChannelBasicInfo): boolean {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
return isAlwaysTrueBranchMapping(branchMapping);
}
export function getStandardBranchId(channelInfo: ChannelBasicInfo): string {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
assertAlwaysTrueBranchMapping(branchMapping);
return getBranchIdFromStandardMapping(branchMapping);
}
export function isEmptyBranchMapping(
branchMapping: BranchMapping
): branchMapping is EmptyBranchMapping {
return branchMapping.data.length === 0;
}
export function isAlwaysTrueBranchMapping(
branchMapping: BranchMapping
): branchMapping is AlwaysTrueBranchMapping {
const numBranches = branchMapping.data.length;
if (numBranches !== 1) {
return false;
}
const branchMappingLogic = branchMapping.data[0].branchMappingLogic;
return isAlwaysTrue(branchMappingLogic);
}
function getBranchIdFromStandardMapping(branchMapping: AlwaysTrueBranchMapping): string {
return branchMapping.data[0].branchId;
}
export function getBranchIds(branchMapping: BranchMapping): string[] {
return branchMapping.data.map(data => data.branchId);
}
export function getBranchMapping(branchMappingString: string): BranchMapping {
try {
return JSON.parse(branchMappingString);
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
}
export function getNodesFromStatement(statement: BranchMappingStatement): BranchMappingNode[] {
return statement.slice(1) as BranchMappingNode[];
}
export function isAndStatement(
statement: BranchMappingStatement
): statement is BranchMappingStatement {
return statement[0] === 'and';
}
export function isStatement(node: BranchMappingNode): node is BranchMappingStatement {
return Array.isArray(node);
}
export function isNodeObject(node: BranchMappingNode): node is BranchMappingObject {
return typeof node === 'object' && !isStatement(node);
}
export function andStatement(nodes: BranchMappingNode[]): ['and', ...BranchMappingNode[]] {
return ['and', ...nodes];
}
export function isAlwaysTrue(node: BranchMappingNode): boolean {
return node === 'true';
}
export function alwaysTrue(): BranchMappingAlwaysTrue {
return 'true';
}
export function equalsOperator(): BranchMappingOperator {
return '==';
}
export function hashLtOperator(): BranchMappingOperator {
return 'hash_lt';
}
function isVersion(branchMapping: BranchMapping, version: number): boolean {
return branchMapping.version === version;
}
export function assertVersion(channelInfo: ChannelBasicInfo, version: number): void {
const branchMapping = getBranchMapping(channelInfo.branchMapping);
if (!isVersion(branchMapping, version)) {
throw new BranchMappingValidationError(
`Expected branch mapping version ${version}. Received: ${JSON.stringify(branchMapping)}`
);
}
}
export function assertStatement(node: BranchMappingNode): asserts node is BranchMappingStatement {
if (!isStatement(node)) {
throw new BranchMappingValidationError(
'Branch mapping node must be a statement. Received: ' + JSON.stringify(node)
);
}
}
export function assertNodeObject(node: BranchMappingNode): asserts node is BranchMappingObject {
if (!isNodeObject(node)) {
throw new BranchMappingValidationError(
'Branch mapping node must be an object. Received: ' + JSON.stringify(node)
);
}
}
export function assertNumber(operand: string | number | string[]): asserts operand is number {
if (typeof operand !== 'number') {
throw new BranchMappingValidationError(
'Expected a number. Received: ' + JSON.stringify(operand)
);
}
}
export function assertString(operand: string | number | string[]): asserts operand is string {
if (typeof operand !== 'string') {
throw new BranchMappingValidationError(
'Expected a string. Received: ' + JSON.stringify(operand)
);
}
}
function assertAlwaysTrueBranchMapping(
branchMapping: BranchMapping
): asserts branchMapping is AlwaysTrueBranchMapping {
if (!isAlwaysTrueBranchMapping(branchMapping)) {
throw new BranchMappingValidationError(
'Expected standard branch mapping. Received: ' + JSON.stringify(branchMapping)
);
}
}
export class BranchMappingValidationError extends Error {}
@@ -1,68 +0,0 @@
import chalk from 'chalk';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import { getRollout, isRollout } from '../rollout/branch-mapping';
import {
FormattedBranchDescription,
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
import { assertVersion, hasEmptyBranchMap, hasStandardBranchMap } from './branch-mapping';
/**
* Log all the branches associated with the channel and their most recent update group
*/
export function logChannelDetails(channel: UpdateChannelObject): void {
assertVersion(channel, 0);
const doesSupportMapping =
hasEmptyBranchMap(channel) || hasStandardBranchMap(channel) || isRollout(channel);
if (!doesSupportMapping) {
Log.log(chalk.dim('Custom branch mapping detected.'));
return;
}
const branchDescriptionByBranchId = getDescriptionByBranchId(channel);
const entries = Object.entries(branchDescriptionByBranchId);
if (entries.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
entries
.map(([_branchId, description]) => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
}
export function getDescriptionByBranchId(
channel: UpdateChannelObject
): Record<string, FormattedBranchDescription> {
return channel.updateBranches.reduce((acc, branch) => {
const maybeRollout = isRollout(channel) ? getRollout(channel) : null;
let maybePercentOnBranch: number | undefined = undefined;
if (maybeRollout) {
maybePercentOnBranch =
maybeRollout.rolledOutBranchId === branch.id
? maybeRollout.percentRolledOut
: 100 - maybeRollout.percentRolledOut;
}
if (branch.updateGroups.length === 0) {
acc[branch.id] = { branch: branch.name, branchRolloutPercentage: maybePercentOnBranch };
return acc;
}
const updateGroupsWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
);
// display the most recent update group
const updateGroupWithBranchDescription = updateGroupsWithBranchDescriptions[0];
const { branch: branchName, ...updateGroup } = updateGroupWithBranchDescription;
acc[branch.id] = {
branch: branchName,
branchRolloutPercentage: maybePercentOnBranch,
update: updateGroup,
};
return acc;
}, {} as Record<string, FormattedBranchDescription>);
}
+3 -108
View File
@@ -1,9 +1,6 @@
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { createUpdateBranchOnAppAsync } from '../branch/queries';
import { BranchNotFoundError } from '../branch/utils';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { PaginatedQueryOptions } from '../commandUtils/pagination';
import { withErrorHandlingAsync } from '../graphql/client';
@@ -15,18 +12,14 @@ import {
} from '../graphql/generated';
import { BranchQuery, UpdateBranchOnChannelObject } from '../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../graphql/types/UpdateChannelBasicInfo';
import Log from '../log';
import { getDisplayNameForProjectIdAsync } from '../project/projectUtils';
import formatFields from '../utils/formatFields';
import { printJsonOnlyOutput } from '../utils/json';
import {
paginatedQueryWithConfirmPromptAsync,
paginatedQueryWithSelectPromptAsync,
} from '../utils/queries';
import { ChannelNotFoundError } from './errors';
import { logChannelDetails } from './print-utils';
import { ChannelBasicInfo } from './utils';
import { logChannelDetails } from './utils';
export const CHANNELS_LIMIT = 25;
@@ -234,11 +227,11 @@ export async function createChannelOnAppAsync(
updateChannel {
createUpdateChannelForApp(appId: $appId, name: $name, branchMapping: $branchMapping) {
id
...UpdateChannelBasicInfoFragment
name
branchMapping
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{
appId,
@@ -269,101 +262,3 @@ export async function ensureChannelExistsAsync(
}
}
}
export async function doesChannelExistAsync(
graphqlClient: ExpoGraphqlClient,
{ appId, channelName }: { appId: string; channelName: string }
): Promise<boolean> {
try {
await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId,
channelName,
});
} catch (err) {
if (err instanceof ChannelNotFoundError) {
return false;
}
throw err;
}
return true;
}
/**
*
* Creates a channel and links it to a branch with the same name.
*
* @param appId the app ID, also known as the project ID
* @param channelName the name of the channel to create
* @param shouldPrintJson print only the JSON output
*/
export async function createAndLinkChannelAsync(
graphqlClient: ExpoGraphqlClient,
{
appId,
channelName,
shouldPrintJson,
}: { appId: string; channelName: string; shouldPrintJson?: boolean }
): Promise<ChannelBasicInfo> {
let branchId: string;
let branchMessage: string;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId,
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
}
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId,
channelName,
branchId,
});
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${appId}`
);
}
if (shouldPrintJson) {
printJsonOnlyOutput(newChannel);
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, appId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
}
return newChannel;
}
+109 -27
View File
@@ -1,35 +1,117 @@
export type ChannelBasicInfo = {
id: string;
name: string;
branchMapping: string;
};
export type BranchBasicInfo = {
id: string;
name: string;
import assert from 'assert';
import chalk from 'chalk';
import { UpdateChannelObject } from '../graphql/queries/ChannelQuery';
import Log from '../log';
import {
FormattedBranchDescription,
formatBranch,
getUpdateGroupDescriptionsWithBranch,
} from '../update/utils';
export type BranchMapping = {
version: number;
data: {
branchId: string;
branchMappingLogic: {
operand: number;
clientKey: string;
branchMappingOperator: string;
} & string;
}[];
};
export type UpdateChannelInfoWithBranches<Branch extends BranchBasicInfo> = ChannelBasicInfo & {
updateBranches: Branch[];
};
/**
* Get the branch mapping and determine whether it is a rollout.
* Ensure that the branch mapping is properly formatted.
*/
export function getBranchMapping(branchMappingString?: string): {
branchMapping: BranchMapping;
isRollout: boolean;
rolloutPercent?: number;
} {
if (!branchMappingString) {
throw new Error('Missing branch mapping.');
}
let branchMapping: BranchMapping;
try {
branchMapping = JSON.parse(branchMappingString);
} catch {
throw new Error(`Could not parse branchMapping string into a JSON: "${branchMappingString}"`);
}
assert(branchMapping, 'Branch Mapping must be defined.');
function getUpdateBranchNullable<Branch extends BranchBasicInfo>(
channel: UpdateChannelInfoWithBranches<Branch>,
branchId: string
): Branch | null {
const updateBranches = channel.updateBranches;
const updateBranch = updateBranches.find(branch => branch.id === branchId);
return updateBranch ?? null;
if (branchMapping.version !== 0) {
throw new Error('Branch mapping must be version 0.');
}
const isRollout = branchMapping.data.length === 2;
const rolloutPercent = branchMapping.data[0]?.branchMappingLogic.operand;
switch (branchMapping.data.length) {
case 0:
break;
case 1:
if (branchMapping.data[0].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a single branch must be "true"');
}
break;
case 2:
if (branchMapping.data[0].branchMappingLogic.clientKey !== 'rolloutToken') {
throw new Error('Client key of initial branch mapping must be "rolloutToken"');
}
if (branchMapping.data[0].branchMappingLogic.branchMappingOperator !== 'hash_lt') {
throw new Error('Branch mapping operator of initial branch mapping must be "hash_lt"');
}
if (rolloutPercent == null) {
throw new Error('Branch mapping is missing a "rolloutPercent"');
}
if (branchMapping.data[1].branchMappingLogic !== 'true') {
throw new Error('Branch mapping logic for a the second branch of a rollout must be "true"');
}
break;
default:
throw new Error('Branch mapping data must have length less than or equal to 2.');
}
return { branchMapping, isRollout, rolloutPercent };
}
export function getUpdateBranch<Branch extends BranchBasicInfo>(
channel: UpdateChannelInfoWithBranches<Branch>,
branchId: string
): Branch {
const updateBranch = getUpdateBranchNullable(channel, branchId);
if (!updateBranch) {
throw new Error(
`Could not find branch with id "${branchId}" in branch-mapping of channel "${channel.name}"`
export function logChannelDetails(channel: UpdateChannelObject): void {
const { branchMapping, isRollout, rolloutPercent } = getBranchMapping(channel.branchMapping);
if (branchMapping.data.length > 2) {
throw new Error('Branch Mapping data must have length less than or equal to 2.');
}
const rolloutBranchIds = branchMapping.data.map(data => data.branchId);
const branchDescription = channel.updateBranches.flatMap(branch => {
const updateGroupWithBranchDescriptions = getUpdateGroupDescriptionsWithBranch(
branch.updateGroups
);
const isRolloutBranch = isRollout && rolloutBranchIds.includes(branch.id);
const isBaseBranch = rolloutBranchIds.length > 0 && rolloutBranchIds[0] === branch.id;
let rolloutPercentNumber: number | undefined = undefined;
if (isRolloutBranch) {
rolloutPercentNumber = isBaseBranch ? rolloutPercent! * 100 : (1 - rolloutPercent!) * 100;
}
return updateGroupWithBranchDescriptions.map(
({ branch, ...updateGroup }): FormattedBranchDescription => ({
branch,
branchRolloutPercentage: rolloutPercentNumber,
update: updateGroup,
})
);
});
if (branchDescription.length === 0) {
Log.log(chalk.dim('No branches are pointed to this channel.'));
} else {
Log.log(
branchDescription
.map(description => formatBranch(description))
.join(`\n\n${chalk.dim('———')}\n\n`)
);
}
return updateBranch;
}
@@ -1,6 +1,5 @@
import { Command } from '@oclif/core';
import { CombinedError } from '@urql/core';
import { GraphQLError } from 'graphql/error';
import nullthrows from 'nullthrows';
import {
@@ -22,7 +21,6 @@ import { OptionalPrivateProjectConfigContextField } from './context/OptionalPriv
import { PrivateProjectConfigContextField } from './context/PrivateProjectConfigContextField';
import ProjectDirContextField from './context/ProjectDirContextField';
import SessionManagementContextField from './context/SessionManagementContextField';
import VcsClientContextField from './context/VcsClientContextField';
import { EasCommandError } from './errors';
export type ContextInput<
@@ -102,9 +100,6 @@ export default abstract class EasCommand extends Command {
Analytics: {
analytics: new AnalyticsContextField(),
},
Vcs: {
vcsClient: new VcsClientContextField(),
},
};
/**
@@ -198,22 +193,10 @@ export default abstract class EasCommand extends Command {
if (err instanceof EasCommandError) {
Log.error(err.message);
} else if (err instanceof CombinedError && err?.graphQLErrors) {
const cleanMessage = err?.graphQLErrors
.map((graphQLError: GraphQLError) => {
const messageLine = graphQLError.message.replace('[GraphQL] ', '');
const requestIdLine = graphQLError.extensions?.requestId
? `\nRequest ID: ${err.graphQLErrors[0].extensions.requestId}`
: '';
const defaultMsg = `${messageLine}${requestIdLine}`;
if (graphQLError.extensions?.errorCode === 'UNAUTHORIZED_ERROR') {
return `You don't have the required permissions to perform this operation.\n\n${defaultMsg}`;
}
return defaultMsg;
})
.join('\n');
let cleanMessage = err.message.replace('[GraphQL] ', '');
cleanMessage += err?.graphQLErrors?.[0]?.extensions?.requestId
? `\nRequest ID: ${err.graphQLErrors[0].extensions.requestId}`
: '';
Log.error(cleanMessage);
baseMessage = BASE_GRAPHQL_ERROR_MESSAGE;
} else {
@@ -172,44 +172,6 @@ describe(EasCommand.name, () => {
expect(logDebugSpy).toBeCalledWith(error);
});
it('logs the cleaned messages with request IDs if multiple GraphQL errors present', async () => {
const TestEasCommand = createTestEasCommand();
const logErrorSpy = jest.spyOn(Log, 'error');
const logDebugSpy = jest.spyOn(Log, 'debug');
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
const graphQLErrors = [
new GraphQLError('Unexpected GraphQL error message', null, null, null, null, null, {
errorCode: 'UNKNOWN_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
new GraphQLError('Other GraphQL error message', null, null, null, null, null, {
errorCode: 'OTHER_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
new GraphQLError('Yet another GraphQL error message', null, null, null, null, null, {
errorCode: 'YET_ANOTHER_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
];
const error = new CombinedError({ graphQLErrors });
runAsyncMock.mockImplementation(() => {
throw error;
});
try {
await TestEasCommand.run();
} catch {}
expect(logErrorSpy).toBeCalledWith(
'Unexpected GraphQL error message\n' +
`Request ID: ${mockRequestId}\n` +
'Other GraphQL error message\n' +
`Request ID: ${mockRequestId}\n` +
'Yet another GraphQL error message\n' +
`Request ID: ${mockRequestId}`
);
expect(logDebugSpy).toBeCalledWith(error);
});
it('re-throws the error with default base message', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
@@ -224,7 +186,7 @@ describe(EasCommand.name, () => {
}
});
it('re-throws the error with a different default base message in case of CombinedError', async () => {
it('re-throws the error with a different default base message in case of graphQLError', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
@@ -238,34 +200,6 @@ describe(EasCommand.name, () => {
expect((caughtError as Error).message).toEqual('GraphQL request failed.');
}
});
it('re-throws the error with a different default base message in case of CombinedError with multiple GraphQLErrors', async () => {
const TestEasCommand = createTestEasCommand();
const runAsyncMock = jest.spyOn(TestEasCommand.prototype as any, 'runAsync');
runAsyncMock.mockImplementation(() => {
const graphQLErrors = [
new GraphQLError('Unexpected GraphQL error message', null, null, null, null, null, {
errorCode: 'UNEXPECTED_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
new GraphQLError('Other GraphQL error message', null, null, null, null, null, {
errorCode: 'OTHER_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
new GraphQLError('Yet another GraphQL error message', null, null, null, null, null, {
errorCode: 'YET_ANOTHER_GRAPHQL_ERROR',
requestId: mockRequestId,
}),
];
throw new CombinedError({ graphQLErrors });
});
try {
await TestEasCommand.run();
} catch (caughtError) {
expect(caughtError).toBeInstanceOf(Error);
expect((caughtError as Error).message).toEqual('GraphQL request failed.');
}
});
});
});
});
@@ -1,9 +0,0 @@
import { getVcsClient } from '../../vcs';
import { Client } from '../../vcs/vcs';
import ContextField from './ContextField';
export default class VcsClientContextField extends ContextField<Client> {
async getValueAsync(): Promise<Client> {
return getVcsClient();
}
}
@@ -1,4 +1,4 @@
import { getConfig, getConfigFilePaths, modifyConfigAsync } from '@expo/config';
import { getConfig, modifyConfigAsync } from '@expo/config';
import { vol } from 'memfs';
import { anything, instance, mock, when } from 'ts-mockito';
@@ -26,10 +26,6 @@ describe(getProjectIdAsync, () => {
let sessionManager: SessionManager;
beforeEach(() => {
jest
.mocked(getConfigFilePaths)
.mockReturnValue({ staticConfigPath: null, dynamicConfigPath: null });
const sessionManagerMock = mock<SessionManager>();
when(sessionManagerMock.ensureLoggedInAsync(anything())).thenResolve({
actor: {
@@ -3,10 +3,11 @@ import {
Client,
OperationContext,
OperationResult,
OperationResultSource,
PromisifiedSource,
TypedDocumentNode,
cacheExchange,
createClient as createUrqlClient,
dedupExchange,
fetchExchange,
} from '@urql/core';
import { retryExchange } from '@urql/exchange-retry';
@@ -21,7 +22,7 @@ export interface ExpoGraphqlClient extends Client {
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables: Variables,
context: Partial<OperationContext> & { additionalTypenames: string[] }
): OperationResultSource<OperationResult<Data, Variables>>;
): PromisifiedSource<OperationResult<Data, Variables>>;
}
export function createGraphqlClient(authInfo: {
@@ -31,6 +32,7 @@ export function createGraphqlClient(authInfo: {
return createUrqlClient({
url: getExpoApiBaseUrl() + '/graphql',
exchanges: [
dedupExchange,
cacheExchange,
retryExchange({
maxDelayMs: 4000,
@@ -27,7 +27,6 @@ export default class BranchCreate extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -38,7 +37,6 @@ export default class BranchCreate extends EasCommand {
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(BranchCreate, {
nonInteractive,
});
@@ -58,7 +56,7 @@ export default class BranchCreate extends EasCommand {
type: 'text',
name: 'name',
message: 'Provide a branch name:',
initial: await getDefaultBranchNameAsync(vcsClient),
initial: await getDefaultBranchNameAsync(),
validate: value => (value ? true : validationMessage),
}));
}
@@ -139,7 +139,6 @@ export default class BuildCancel extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -11,11 +11,9 @@ import { isExpoUpdatesInstalled, isUsingEASUpdate } from '../../project/projectU
import { resolveWorkflowAsync } from '../../project/workflow';
import { promptAsync } from '../../prompts';
import { syncUpdatesConfigurationAsync as syncAndroidUpdatesConfigurationAsync } from '../../update/android/UpdatesModule';
import {
ensureEASUpdateIsConfiguredInEasJsonAsync,
ensureUseClassicUpdatesIsRemovedAsync,
} from '../../update/configure';
import { ensureEASUpdateIsConfiguredInEasJsonAsync } from '../../update/configure';
import { syncUpdatesConfigurationAsync as syncIosUpdatesConfigurationAsync } from '../../update/ios/UpdatesModule';
import { getVcsClient } from '../../vcs';
export default class BuildConfigure extends EasCommand {
static override description = 'configure the project to support EAS Build';
@@ -31,7 +29,6 @@ export default class BuildConfigure extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -39,7 +36,6 @@ export default class BuildConfigure extends EasCommand {
const {
privateProjectConfig: { exp, projectId, projectDir },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(BuildConfigure, {
nonInteractive: false,
});
@@ -48,9 +44,7 @@ export default class BuildConfigure extends EasCommand {
'💡 The following process will configure your iOS and/or Android project to be compatible with EAS Build. These changes only apply to your local project files and you can safely revert them at any time.'
);
// BuildConfigure.ContextOptions.Vcs.client.getValueAsync()
await vcsClient.ensureRepoExistsAsync();
await getVcsClient().ensureRepoExistsAsync();
const expoUpdatesIsInstalled = isExpoUpdatesInstalled(projectDir);
@@ -67,37 +61,24 @@ export default class BuildConfigure extends EasCommand {
const didCreateEasJson = await ensureProjectConfiguredAsync({
projectDir,
nonInteractive: false,
vcsClient,
});
if (didCreateEasJson && isUsingEASUpdate(exp, projectId)) {
if (exp.updates?.useClassicUpdates) {
// NOTE: this method modifies the Expo config; be sure to use this function's return value
// if the config object is used later in the future
await ensureUseClassicUpdatesIsRemovedAsync({ exp, projectDir });
}
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
}
// configure expo-updates
if (expoUpdatesIsInstalled) {
if ([RequestedPlatform.Android, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
if (workflow === Workflow.GENERIC) {
await syncAndroidUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
}
if ([RequestedPlatform.Ios, RequestedPlatform.All].includes(platform)) {
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (workflow === Workflow.GENERIC) {
await syncIosUpdatesConfigurationAsync(
graphqlClient,
vcsClient,
projectDir,
exp,
projectId
);
await syncIosUpdatesConfigurationAsync(graphqlClient, projectDir, exp, projectId);
}
}
}
@@ -106,7 +106,6 @@ export default class Build extends EasCommand {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -122,7 +121,6 @@ export default class Build extends EasCommand {
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
} = await this.getContextAsync(Build, {
nonInteractive: flags.nonInteractive,
});
@@ -143,7 +141,6 @@ export default class Build extends EasCommand {
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
flagsWithPlatform,
actor,
@@ -11,6 +11,7 @@ import Log from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import { getTmpDirectory } from '../../utils/paths';
import { getVcsClient } from '../../vcs';
enum InspectStage {
ARCHIVE = 'archive',
@@ -66,7 +67,6 @@ export default class BuildInspect extends EasCommand {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -76,7 +76,6 @@ export default class BuildInspect extends EasCommand {
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
} = await this.getContextAsync(BuildInspect, {
nonInteractive: false,
});
@@ -93,15 +92,15 @@ export default class BuildInspect extends EasCommand {
await this.prepareOutputDirAsync(outputDirectory, flags.force);
if (flags.stage === InspectStage.ARCHIVE) {
await vcsClient.ensureRepoExistsAsync();
await vcsClient.makeShallowCopyAsync(tmpWorkingdir);
const vcs = getVcsClient();
await vcs.ensureRepoExistsAsync();
await vcs.makeShallowCopyAsync(tmpWorkingdir);
await this.copyToOutputDirAsync(tmpWorkingdir, outputDirectory);
} else {
try {
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
{
nonInteractive: false,
@@ -37,7 +37,6 @@ export default class BuildInternal extends EasCommand {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -50,7 +49,6 @@ export default class BuildInternal extends EasCommand {
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
} = await this.getContextAsync(BuildInternal, {
nonInteractive: true,
});
@@ -62,7 +60,6 @@ export default class BuildInternal extends EasCommand {
await runBuildAndSubmitAsync(
graphqlClient,
analytics,
vcsClient,
projectDir,
{
requestedPlatform: flags.platform as RequestedPlatform,
@@ -56,7 +56,6 @@ export default class BuildList extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -88,7 +88,6 @@ export default class BuildResign extends EasCommand {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -105,7 +104,6 @@ export default class BuildResign extends EasCommand {
getDynamicPrivateProjectConfigAsync,
projectDir,
analytics,
vcsClient,
} = await this.getContextAsync(BuildResign, {
nonInteractive: flags.nonInteractive,
});
@@ -147,7 +145,6 @@ export default class BuildResign extends EasCommand {
analytics,
env: buildProfile.env,
easJsonCliConfig,
vcsClient,
});
if (buildProfile.credentialsSource !== CredentialsSource.LOCAL && !nonInteractive) {
await credentialsCtx.appStore.ensureAuthenticatedAsync();
@@ -157,7 +154,6 @@ export default class BuildResign extends EasCommand {
projectDir,
nonInteractive,
exp,
vcsClient,
},
buildProfile
);
@@ -166,7 +162,6 @@ export default class BuildResign extends EasCommand {
exp,
xcodeBuildContext,
env: buildProfile.env,
vcsClient,
});
const credentialsResult = await ensureIosCredentialsForBuildResignAsync(
credentialsCtx,
@@ -204,6 +199,8 @@ export default class BuildResign extends EasCommand {
const buildResult = await waitForBuildEndAsync(graphqlClient, {
buildIds: [newBuild.id],
accountName: account.name,
nonInteractive,
projectDir,
});
if (!flags.json) {
printBuildResults(buildResult);
+1 -17
View File
@@ -33,7 +33,6 @@ interface RawRunFlags {
platform?: string;
limit?: number;
offset?: number;
profile?: string;
}
interface RunCommandFlags {
@@ -41,7 +40,6 @@ interface RunCommandFlags {
runArchiveFlags: RunArchiveFlags;
limit?: number;
offset?: number;
profile?: string;
}
export default class Run extends EasCommand {
@@ -68,12 +66,6 @@ export default class Run extends EasCommand {
char: 'p',
options: ['android', 'ios'],
}),
profile: Flags.string({
char: 'e',
description:
'Name of the build profile used to create the build to run. When specified, only builds created with the specified build profile will be queried.',
helpValue: 'PROFILE_NAME',
}),
...EasPaginatedQueryFlags,
};
@@ -81,7 +73,6 @@ export default class Run extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -106,7 +97,7 @@ export default class Run extends EasCommand {
}
private async sanitizeFlagsAsync(flags: RawRunFlags): Promise<RunCommandFlags> {
const { platform, limit, offset, profile, ...runArchiveFlags } = flags;
const { platform, limit, offset, ...runArchiveFlags } = flags;
const selectedPlatform = await resolvePlatformAsync(platform);
@@ -130,16 +121,11 @@ export default class Run extends EasCommand {
});
}
if (profile && (runArchiveFlags.id || runArchiveFlags.path || runArchiveFlags.url)) {
Log.warn('The --profile flag is ignored when using --id, --path, or --url flags.');
}
return {
selectedPlatform,
runArchiveFlags,
limit,
offset,
profile,
};
}
}
@@ -221,7 +207,6 @@ async function maybeGetBuildAsync(
platform: flags.selectedPlatform,
distribution: distributionType,
status: BuildStatus.Finished,
buildProfile: flags.profile,
},
paginatedQueryOptions,
selectPromptDisabledFunction: build => !isRunnableOnSimulatorOrEmulator(build),
@@ -237,7 +222,6 @@ async function maybeGetBuildAsync(
platform: flags.selectedPlatform,
distribution: distributionType,
status: BuildStatus.Finished,
buildProfile: flags.profile,
},
});
@@ -38,7 +38,6 @@ export default class BuildVersionGetView extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
public async runAsync(): Promise<void> {
@@ -50,7 +49,6 @@ export default class BuildVersionGetView extends EasCommand {
loggedIn: { graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
vcsClient,
} = await this.getContextAsync(BuildVersionGetView, {
nonInteractive: true,
});
@@ -70,7 +68,6 @@ export default class BuildVersionGetView extends EasCommand {
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
const results: { [key in Platform]?: string } = {};
for (const { profile, platform } of buildProfiles) {
@@ -87,7 +84,6 @@ export default class BuildVersionGetView extends EasCommand {
exp,
buildProfile: profile,
platform,
vcsClient,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -41,7 +41,6 @@ export default class BuildVersionSetView extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
public async runAsync(): Promise<void> {
@@ -50,7 +49,6 @@ export default class BuildVersionSetView extends EasCommand {
loggedIn: { graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
vcsClient,
} = await this.getContextAsync(BuildVersionSetView, {
nonInteractive: false,
});
@@ -76,7 +74,6 @@ export default class BuildVersionSetView extends EasCommand {
exp,
buildProfile: profile,
platform,
vcsClient,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -28,7 +28,6 @@ import {
} from '../../../project/remoteVersionSource';
import { resolveWorkflowAsync } from '../../../project/workflow';
import { getProfilesAsync } from '../../../utils/profiles';
import { Client } from '../../../vcs/vcs';
interface SyncContext<T extends Platform> {
projectDir: string;
@@ -36,7 +35,6 @@ interface SyncContext<T extends Platform> {
workflow: Workflow;
profile: BuildProfile<T>;
buildVersion: string;
vcsClient: Client;
}
export default class BuildVersionSyncView extends EasCommand {
@@ -60,7 +58,6 @@ export default class BuildVersionSyncView extends EasCommand {
...this.ContextOptions.LoggedIn,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
public async runAsync(): Promise<void> {
@@ -69,7 +66,6 @@ export default class BuildVersionSyncView extends EasCommand {
loggedIn: { graphqlClient },
getDynamicPrivateProjectConfigAsync,
projectDir,
vcsClient,
} = await this.getContextAsync(BuildVersionSyncView, {
nonInteractive: true,
});
@@ -84,7 +80,6 @@ export default class BuildVersionSyncView extends EasCommand {
easJsonAccessor,
platforms,
profileName: flags.profile ?? undefined,
projectDir,
});
for (const profileInfo of buildProfiles) {
const { exp, projectId } = await getDynamicPrivateProjectConfigAsync({
@@ -101,7 +96,6 @@ export default class BuildVersionSyncView extends EasCommand {
exp,
buildProfile: profileInfo.profile,
platform: profileInfo.platform,
vcsClient,
});
const remoteVersions = await AppVersionQuery.latestVersionAsync(
graphqlClient,
@@ -109,7 +103,7 @@ export default class BuildVersionSyncView extends EasCommand {
toAppPlatform(profileInfo.platform),
applicationIdentifier
);
const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform, vcsClient);
const workflow = await resolveWorkflowAsync(projectDir, profileInfo.platform);
if (!remoteVersions?.buildVersion) {
Log.warn(
`Skipping versions sync for ${platformDisplayName}. There are no versions configured on Expo servers, use "eas build:version:set" or run a build to initialize it.`
@@ -133,7 +127,6 @@ export default class BuildVersionSyncView extends EasCommand {
profile: profileInfo.profile as BuildProfile<Platform.ANDROID>,
workflow,
buildVersion: remoteVersions.buildVersion,
vcsClient,
});
} else {
this.syncIosAsync({
@@ -142,7 +135,6 @@ export default class BuildVersionSyncView extends EasCommand {
profile: profileInfo.profile as BuildProfile<Platform.IOS>,
workflow,
buildVersion: remoteVersions.buildVersion,
vcsClient,
});
}
Log.withTick(
@@ -159,10 +151,9 @@ export default class BuildVersionSyncView extends EasCommand {
exp,
profile,
buildVersion,
vcsClient,
}: SyncContext<Platform.IOS>): Promise<void> {
const xcodeBuildContext = await resolveXcodeBuildContextAsync(
{ exp, projectDir, nonInteractive: false, vcsClient },
{ exp, projectDir, nonInteractive: false },
profile
);
const targets = await resolveTargetsAsync({
@@ -170,7 +161,6 @@ export default class BuildVersionSyncView extends EasCommand {
exp,
xcodeBuildContext,
env: profile.env,
vcsClient,
});
if (!isValidBuildNumber(buildVersion)) {
@@ -20,7 +20,6 @@ export default class BuildView extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -1,11 +1,16 @@
import chalk from 'chalk';
import { createAndLinkChannelAsync } from '../../channel/queries';
import { createUpdateBranchOnAppAsync } from '../../branch/queries';
import { BranchNotFoundError } from '../../branch/utils';
import { createChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { enableJsonOutput } from '../../utils/json';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export default class ChannelCreate extends EasCommand {
static override description = 'create a channel';
@@ -55,13 +60,69 @@ export default class ChannelCreate extends EasCommand {
}));
}
await createAndLinkChannelAsync(graphqlClient, {
let branchId: string;
let branchMessage: string;
try {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: channelName,
});
branchId = branch.id;
branchMessage = `We found a branch with the same name`;
} catch (error) {
if (error instanceof BranchNotFoundError) {
const newBranch = await createUpdateBranchOnAppAsync(graphqlClient, {
appId: projectId,
name: channelName,
});
branchId = newBranch.id;
branchMessage = `We also went ahead and made a branch with the same name`;
} else {
throw error;
}
}
const {
updateChannel: { createUpdateChannelForApp: newChannel },
} = await createChannelOnAppAsync(graphqlClient, {
appId: projectId,
channelName,
shouldPrintJson: jsonFlag,
branchId,
});
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
if (!newChannel) {
throw new Error(
`Could not create channel with name ${channelName} on project with id ${projectId}`
);
}
if (jsonFlag) {
printJsonOnlyOutput(newChannel);
} else {
Log.addNewLineIfNone();
Log.withTick(
`Created a new channel on project ${chalk.bold(
await getDisplayNameForProjectIdAsync(graphqlClient, projectId)
)}`
);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: newChannel.id },
])
);
Log.addNewLineIfNone();
Log.withTick(`${branchMessage} and have pointed the channel at it.`);
Log.log(
formatFields([
{ label: 'Name', value: newChannel.name },
{ label: 'ID', value: branchId },
])
);
Log.addNewLineIfNone();
Log.log(chalk.bold('You can now update your app by publishing!'));
}
}
}
+4 -11
View File
@@ -1,31 +1,26 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { print } from 'graphql';
import gql from 'graphql-tag';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { hasEmptyBranchMap, hasStandardBranchMap } from '../../channel/branch-mapping';
import { selectChannelOnAppAsync } from '../../channel/queries';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { withErrorHandlingAsync } from '../../graphql/client';
import {
UpdateChannelBasicInfoFragment,
UpdateChannelBranchMappingMutation,
UpdateChannelBranchMappingMutationVariables,
} from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery } from '../../graphql/queries/ChannelQuery';
import { UpdateChannelBasicInfoFragmentNode } from '../../graphql/types/UpdateChannelBasicInfo';
import Log from '../../log';
import { isRollout } from '../../rollout/branch-mapping';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
export async function updateChannelBranchMappingAsync(
graphqlClient: ExpoGraphqlClient,
{ channelId, branchMapping }: UpdateChannelBranchMappingMutationVariables
): Promise<UpdateChannelBasicInfoFragment> {
): Promise<UpdateChannelBranchMappingMutation['updateChannel']['editUpdateChannel']> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<UpdateChannelBranchMappingMutation, UpdateChannelBranchMappingMutationVariables>(
@@ -34,11 +29,11 @@ export async function updateChannelBranchMappingAsync(
updateChannel {
editUpdateChannel(channelId: $channelId, branchMapping: $branchMapping) {
id
...UpdateChannelBasicInfoFragment
name
branchMapping
}
}
}
${print(UpdateChannelBasicInfoFragmentNode)}
`,
{ channelId, branchMapping }
)
@@ -100,10 +95,8 @@ export default class ChannelEdit extends EasCommand {
paginatedQueryOptions: { json, nonInteractive, offset: 0 },
});
if (isRollout(existingChannel)) {
if (existingChannel.updateBranches.length > 1) {
throw new Error('There is a rollout in progress. Manage it with "channel:rollout" instead.');
} else if (!hasStandardBranchMap(existingChannel) && !hasEmptyBranchMap(existingChannel)) {
throw new Error('Only standard branch mappings can be edited with this command.');
}
const branch = branchFlag
+441 -140
View File
@@ -1,50 +1,297 @@
import { Flags } from '@oclif/core';
import chalk from 'chalk';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import { BranchMapping, getBranchMapping } from '../../channel/utils';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { NonInteractiveOptions as CreateRolloutNonInteractiveOptions } from '../../rollout/actions/CreateRollout';
import { NonInteractiveOptions as EditRolloutNonInteractiveOptions } from '../../rollout/actions/EditRollout';
import {
EndOutcome,
GeneralOptions as EndRolloutGeneralOptions,
NonInteractiveOptions as EndRolloutNonInteractiveOptions,
} from '../../rollout/actions/EndRollout';
import { ManageRolloutActions } from '../../rollout/actions/ManageRollout';
import { NonInteractiveRollout } from '../../rollout/actions/NonInteractiveRollout';
import {
MainMenuActions,
RolloutActions,
RolloutMainMenu,
} from '../../rollout/actions/RolloutMainMenu';
import { enableJsonOutput } from '../../utils/json';
import { UpdateBranch } from '../../graphql/generated';
import { BranchQuery } from '../../graphql/queries/BranchQuery';
import { ChannelQuery, UpdateChannelByNameObject } from '../../graphql/queries/ChannelQuery';
import Log from '../../log';
import { getDisplayNameForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync, selectAsync } from '../../prompts';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { updateChannelBranchMappingAsync } from './edit';
enum ActionRawFlagValue {
CREATE = 'create',
EDIT = 'edit',
END = 'end',
VIEW = 'view',
async function promptForRolloutPercentAsync({
promptMessage,
}: {
promptMessage: string;
}): Promise<number> {
const { name: rolloutPercent } = await promptAsync({
type: 'text',
name: 'name',
format: value => {
return parseInt(value, 10);
},
message: promptMessage,
initial: 0,
validate: (rolloutPercent: string): true | string => {
const floatValue = parseFloat(rolloutPercent);
return Number.isInteger(floatValue) && floatValue >= 0 && floatValue <= 100
? true
: 'The rollout percentage must be an integer between 0 and 100 inclusive.';
},
});
return rolloutPercent;
}
type ChannelRolloutRawArgsAndFlags = {
channel?: string;
action?: ActionRawFlagValue;
percent?: number;
outcome?: EndOutcome;
'non-interactive': boolean;
branch?: string;
'runtime-version'?: string;
'private-key-path'?: string;
json?: boolean;
};
type ChannelRolloutArgsAndFlags = {
channelName?: string;
action?: RolloutActions;
nonInteractive: boolean;
json?: boolean;
} & Partial<EditRolloutNonInteractiveOptions> &
Partial<EndRolloutNonInteractiveOptions> &
EndRolloutGeneralOptions &
Partial<CreateRolloutNonInteractiveOptions>;
function getRolloutInfo(channel: UpdateChannelByNameObject): {
newBranch: Pick<UpdateBranch, 'name' | 'id'>;
oldBranch: Pick<UpdateBranch, 'name' | 'id'>;
currentPercent: number;
} {
const { branchMapping } = getBranchMapping(channel.branchMapping);
const [newBranchId, oldBranchId] = branchMapping.data.map(d => d.branchId);
const newBranch = channel.updateBranches.filter(branch => branch.id === newBranchId)[0];
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
if (!newBranch || !oldBranch) {
throw new Error(`Branch mapping rollout is missing a branch for channel "${channel.name}".`);
}
const currentPercent = 100 * branchMapping.data[0].branchMappingLogic.operand;
return { newBranch, oldBranch, currentPercent };
}
async function startRolloutAsync(
graphqlClient: ExpoGraphqlClient,
{
channelName,
branchName,
percent,
projectId,
displayName,
currentBranchMapping,
channel,
nonInteractive,
}: {
channelName?: string;
branchName: string;
percent?: number;
projectId: string;
displayName: string;
currentBranchMapping: BranchMapping;
channel: UpdateChannelByNameObject;
nonInteractive: boolean;
}
): Promise<{
newChannelInfo: {
id: string;
name: string;
branchMapping: string;
};
logMessage: string;
}> {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: branchName,
});
const oldBranchId = currentBranchMapping.data[0].branchId;
if (branch.id === oldBranchId) {
throw new Error(
`channel "${channelName}" is already pointing at branch "${branchName}". Rollouts must be done with distinct branches.`
);
}
if (percent == null) {
if (nonInteractive) {
throw new Error(
'You must specify a percent with the --percent flag when initiating a rollout with the --non-interactive flag.'
);
}
const promptMessage = `What percent of users should be directed to the branch "${branchName}"?`;
percent = await promptForRolloutPercentAsync({ promptMessage });
}
const newBranchMapping = {
version: 0,
data: [
{
branchId: branch.id,
branchMappingLogic: {
operand: percent / 100,
clientKey: 'rolloutToken',
branchMappingOperator: 'hash_lt',
},
},
currentBranchMapping.data[0],
],
};
const newChannelInfo = await updateChannelBranchMappingAsync(graphqlClient, {
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const oldBranch = channel.updateBranches.filter(branch => branch.id === oldBranchId)[0];
if (!oldBranch) {
throw new Error(
`Branch mapping is missing its only branch for channel "${channelName}" on app "${displayName}"`
);
}
const logMessage = `Started a rollout of branch ${chalk.bold(branchName)} on channel ${chalk.bold(
channelName!
)}! ${chalk.bold(percent)}% of users will be directed to branch ${chalk.bold(
branchName
)}, ${chalk.bold(100 - percent)}% to branch ${chalk.bold(oldBranch.name)}.`;
return { newChannelInfo, logMessage };
}
async function editRolloutAsync(
graphqlClient: ExpoGraphqlClient,
{
channelName,
percent,
nonInteractive,
currentBranchMapping,
channel,
}: {
channelName?: string;
percent?: number;
nonInteractive: boolean;
currentBranchMapping: BranchMapping;
channel: UpdateChannelByNameObject;
}
): Promise<{
newChannelInfo: {
id: string;
name: string;
branchMapping: string;
};
logMessage: string;
}> {
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
if (percent == null) {
if (nonInteractive) {
throw new Error(
'A rollout is already in progress. If you wish to modify it you must use specify the new rollout percentage with the --percent flag.'
);
}
const promptMessage = `Currently ${currentPercent}% of all users are routed to branch ${
newBranch.name
} and ${100 - currentPercent}% of all users are routed to branch ${
oldBranch.name
}. What percent of users should be directed to the branch ${newBranch.name}?`;
percent = await promptForRolloutPercentAsync({ promptMessage });
}
const newBranchMapping = { ...currentBranchMapping };
newBranchMapping.data[0].branchMappingLogic.operand = percent / 100;
const newChannelInfo = await updateChannelBranchMappingAsync(graphqlClient, {
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `Rollout of branch ${chalk.bold(newBranch.name)} on channel ${chalk.bold(
channelName!
)} updated from ${chalk.bold(currentPercent)}% to ${chalk.bold(percent)}%. ${chalk.bold(
percent
)}% of users will be directed to branch ${chalk.bold(newBranch.name)}, ${chalk.bold(
100 - percent
)}% to branch ${chalk.bold(oldBranch.name)}.`;
return { newChannelInfo, logMessage };
}
async function endRolloutAsync(
graphqlClient: ExpoGraphqlClient,
{
channelName,
branchName,
nonInteractive,
projectId,
channel,
}: {
channelName?: string;
branchName?: string;
nonInteractive: boolean;
projectId: string;
channel: UpdateChannelByNameObject;
}
): Promise<{
newChannelInfo: {
id: string;
name: string;
branchMapping: string;
};
logMessage: string;
}> {
// end rollout
const { newBranch, oldBranch, currentPercent } = getRolloutInfo(channel);
let endOnNewBranch;
if (branchName) {
const branch = await BranchQuery.getBranchByNameAsync(graphqlClient, {
appId: projectId,
name: branchName,
});
switch (branch.id) {
case newBranch.id:
endOnNewBranch = true;
break;
case oldBranch.id:
endOnNewBranch = false;
break;
default:
throw new Error(
`The branch "${branchName}" specified by --branch must be one of the branches involved in the rollout: "${newBranch.name}" or "${oldBranch.name}".`
);
}
} else {
if (nonInteractive) {
throw new Error(
'Branch name must be specified with the --branch flag when both the --end and --non-interactive flag are true.'
);
}
endOnNewBranch = await selectAsync<boolean>(
'Ending the rollout will send all traffic to a single branch. Which one should that be?',
[
{
title: `${newBranch.name} ${chalk.grey(`- current percent: ${currentPercent}%`)}`,
value: true,
},
{
title: `${oldBranch.name} ${chalk.grey(`- current percent: ${100 - currentPercent}%`)}`,
value: false,
},
]
);
}
if (endOnNewBranch == null) {
throw new Error('Branch to end on is undefined.');
}
const newBranchMapping = {
version: 0,
data: [
{
branchId: endOnNewBranch ? newBranch.id : oldBranch.id,
branchMappingLogic: 'true',
},
],
};
const newChannelInfo = await updateChannelBranchMappingAsync(graphqlClient, {
channelId: channel.id,
branchMapping: JSON.stringify(newBranchMapping),
});
const logMessage = `Rollout on channel ${chalk.bold(
channelName
)} ended. All traffic is now sent to branch ${chalk.bold(
endOnNewBranch ? newBranch.name : oldBranch.name
)}`;
return { newChannelInfo, logMessage };
}
export default class ChannelRollout extends EasCommand {
static override description = 'Roll a new branch out on a channel incrementally.';
@@ -57,68 +304,17 @@ export default class ChannelRollout extends EasCommand {
];
static override flags = {
action: Flags.enum({
description: 'Rollout action to perform',
options: Object.values(ActionRawFlagValue),
branch: Flags.string({
description: 'branch to rollout',
required: false,
relationships: [
{
type: 'all',
flags: [
{
name: 'percent',
// eslint-disable-next-line async-protect/async-suffix
when: async flags => {
return (
!!flags['non-interactive'] &&
(flags['action'] === ActionRawFlagValue.CREATE ||
flags['action'] === ActionRawFlagValue.EDIT)
);
},
},
{
name: 'outcome',
// eslint-disable-next-line async-protect/async-suffix
when: async flags =>
!!flags['non-interactive'] && flags['action'] === ActionRawFlagValue.END,
},
{
name: 'branch',
// eslint-disable-next-line async-protect/async-suffix
when: async flags =>
!!flags['non-interactive'] && flags['action'] === ActionRawFlagValue.CREATE,
},
{
name: 'runtime-version',
// eslint-disable-next-line async-protect/async-suffix
when: async flags =>
!!flags['non-interactive'] && flags['action'] === ActionRawFlagValue.CREATE,
},
],
},
],
}),
percent: Flags.integer({
description:
'Percent of users to send to the new branch. Use with --action=edit or --action=create',
description: 'percent of users to send to the new branch',
required: false,
}),
outcome: Flags.enum({
description: 'End outcome of rollout. Use with --action=end',
options: Object.values(EndOutcome),
required: false,
}),
branch: Flags.string({
description: 'Branch to roll out. Use with --action=create',
required: false,
}),
'runtime-version': Flags.string({
description: 'Runtime version to target. Use with --action=create',
required: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
end: Flags.boolean({
description: 'end the rollout',
default: false,
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -129,59 +325,164 @@ export default class ChannelRollout extends EasCommand {
};
async runAsync(): Promise<void> {
const { args, flags } = await this.parse(ChannelRollout);
const argsAndFlags = this.sanitizeArgsAndFlags({ ...flags, ...args });
const {
privateProjectConfig: { exp, projectId },
args: { channel: channelNameArg },
flags: {
json: jsonFlag,
end: endFlag,
branch: branchName,
percent,
'non-interactive': nonInteractive,
},
} = await this.parse(ChannelRollout);
const {
privateProjectConfig: { projectId },
loggedIn: { graphqlClient },
} = await this.getContextAsync(ChannelRollout, {
nonInteractive: argsAndFlags.nonInteractive,
nonInteractive,
});
if (argsAndFlags.json) {
if (jsonFlag) {
enableJsonOutput();
}
const app = { projectId, exp };
const ctx = {
projectId,
nonInteractive: argsAndFlags.nonInteractive,
graphqlClient,
app,
const projectDisplayName = await getDisplayNameForProjectIdAsync(graphqlClient, projectId);
let channelName: string = channelNameArg;
if (!channelName) {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel on which to perform a rollout',
paginatedQueryOptions: {
json: jsonFlag,
nonInteractive,
offset: 0,
},
});
channelName = name;
}
const channel = await ChannelQuery.viewUpdateChannelAsync(graphqlClient, {
appId: projectId,
channelName,
});
if (!channel) {
throw new Error(
`Could not find a channel named "${channelName}". Check which channels exist on this project with ${chalk.bold(
'eas channel:list'
)}.`
);
}
const { branchMapping: currentBranchMapping, isRollout } = getBranchMapping(
channel.branchMapping
);
if (currentBranchMapping.data.length === 0) {
throw new Error('The channel is not pointing at any branches.');
}
if (currentBranchMapping.data.length > 2) {
throw new Error('"channel:rollout" cannot handle branch mappings with more than 2 branches.');
}
// This combination doesn't make sense. Throw an error explaining the options.
if (isRollout && branchName && !endFlag) {
throw new Error(
`There is a rollout in progress. You can only either edit the rollout percent or 'end' it.`
);
}
/**
* This if/else block has three branches:
* 1. The branch mapping is not a rollout, i.e. it is pointing to a single branch.
* 2. The branch mapping is a rollout.
* a. increase/decrease the rollout percentage.
* b. end the rollout.
*/
let rolloutMutationResult: {
newChannelInfo: {
id: string;
name: string;
branchMapping: string;
};
logMessage: string;
};
if (argsAndFlags.nonInteractive) {
await new NonInteractiveRollout(argsAndFlags).runAsync(ctx);
if (!isRollout) {
rolloutMutationResult = await startRolloutAsync(graphqlClient, {
channelName,
branchName:
branchName ??
(await promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json: jsonFlag,
})),
percent,
nonInteractive,
projectId,
displayName: projectDisplayName,
currentBranchMapping,
channel,
});
} else if (endFlag) {
rolloutMutationResult = await endRolloutAsync(graphqlClient, {
channelName,
branchName,
nonInteractive,
projectId,
channel,
});
} else {
await new RolloutMainMenu(argsAndFlags).runAsync(ctx);
rolloutMutationResult = await editRolloutAsync(graphqlClient, {
channelName,
percent,
nonInteractive,
currentBranchMapping,
channel,
});
}
}
private getAction(action: ActionRawFlagValue): RolloutActions {
switch (action) {
case ActionRawFlagValue.CREATE:
return MainMenuActions.CREATE_NEW;
case ActionRawFlagValue.EDIT:
return ManageRolloutActions.EDIT;
case ActionRawFlagValue.END:
return ManageRolloutActions.END;
case ActionRawFlagValue.VIEW:
return ManageRolloutActions.VIEW;
if (!rolloutMutationResult) {
throw new Error('rollout result is empty');
}
const { newChannelInfo, logMessage } = rolloutMutationResult;
if (jsonFlag) {
printJsonOnlyOutput(newChannelInfo);
} else {
Log.withTick(logMessage);
}
}
private sanitizeArgsAndFlags(
rawFlags: ChannelRolloutRawArgsAndFlags
): ChannelRolloutArgsAndFlags {
const action = rawFlags.action;
return {
channelName: rawFlags.channel,
percent: rawFlags.percent,
outcome: rawFlags.outcome,
branchNameToRollout: rawFlags.branch,
runtimeVersion: rawFlags['runtime-version'],
privateKeyPath: rawFlags['private-key-path'] ?? null,
action: action ? this.getAction(action) : undefined,
nonInteractive: rawFlags['non-interactive'],
json: rawFlags.json,
};
}
}
async function promptForBranchNameAsync({
graphqlClient,
projectId,
channelName,
nonInteractive,
json,
}: {
graphqlClient: ExpoGraphqlClient;
projectId: string;
channelName: string;
nonInteractive: boolean;
json: boolean;
}): Promise<string> {
if (nonInteractive) {
throw new Error('Must supply branch flag in non-interactive mode');
}
const { name: branchName } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: `Which branch would you like roll out on ${channelName}?`,
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json,
nonInteractive,
offset: 0,
},
});
return branchName;
}
@@ -15,7 +15,6 @@ export default class Credentials extends EasCommand {
...this.ContextOptions.OptionalProjectConfig,
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -25,7 +24,6 @@ export default class Credentials extends EasCommand {
privateProjectConfig,
getDynamicPrivateProjectConfigAsync,
analytics,
vcsClient,
} = await this.getContextAsync(Credentials, {
nonInteractive: false,
});
@@ -33,7 +31,6 @@ export default class Credentials extends EasCommand {
await new SelectPlatform(
actor,
graphqlClient,
vcsClient,
analytics,
privateProjectConfig ?? null,
getDynamicPrivateProjectConfigAsync,
+5 -7
View File
@@ -5,18 +5,16 @@ import EasCommand from '../commandUtils/EasCommand';
import Log from '../log';
import { resolveWorkflowAsync } from '../project/workflow';
import { easCliVersion } from '../utils/easCli';
import { Client } from '../vcs/vcs';
export default class Diagnostics extends EasCommand {
static override description = 'display environment info';
static override contextDefinition = {
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
const { projectDir, vcsClient } = await this.getContextAsync(Diagnostics, {
const { projectDir } = await this.getContextAsync(Diagnostics, {
nonInteractive: true,
});
@@ -45,13 +43,13 @@ export default class Diagnostics extends EasCommand {
);
Log.log(info.trimEnd());
await this.printWorkflowAsync(projectDir, vcsClient);
await this.printWorkflowAsync(projectDir);
Log.newLine();
}
private async printWorkflowAsync(projectDir: string, vcsClient: Client): Promise<void> {
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID, vcsClient);
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS, vcsClient);
private async printWorkflowAsync(projectDir: string): Promise<void> {
const androidWorkflow = await resolveWorkflowAsync(projectDir, Platform.ANDROID);
const iosWorkflow = await resolveWorkflowAsync(projectDir, Platform.IOS);
if (androidWorkflow === iosWorkflow) {
Log.log(` Project workflow: ${androidWorkflow}`);
@@ -47,7 +47,6 @@ export default class MetadataLint extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
@@ -27,7 +27,6 @@ export default class MetadataPull extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -38,20 +37,18 @@ export default class MetadataPull extends EasCommand {
loggedIn: { actor, graphqlClient },
privateProjectConfig: { exp, projectId, projectDir },
analytics,
vcsClient,
} = await this.getContextAsync(MetadataPull, {
nonInteractive: false,
});
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false, vcsClient });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
@@ -66,7 +63,6 @@ export default class MetadataPull extends EasCommand {
graphqlClient,
analytics,
nonInteractive: false,
vcsClient,
});
try {
@@ -25,7 +25,6 @@ export default class MetadataPush extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -36,20 +35,18 @@ export default class MetadataPush extends EasCommand {
loggedIn: { actor, graphqlClient },
privateProjectConfig: { exp, projectId, projectDir },
analytics,
vcsClient,
} = await this.getContextAsync(MetadataPush, {
nonInteractive: false,
});
// this command is interactive (all nonInteractive flags passed to utility functions are false)
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false, vcsClient });
await ensureProjectConfiguredAsync({ projectDir, nonInteractive: false });
const submitProfiles = await getProfilesAsync({
type: 'submit',
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms: [Platform.IOS],
profileName: flags.profile,
projectDir,
});
if (submitProfiles.length !== 1) {
@@ -64,7 +61,6 @@ export default class MetadataPush extends EasCommand {
graphqlClient,
analytics,
nonInteractive: false,
vcsClient,
});
try {
+15 -70
View File
@@ -16,7 +16,7 @@ import { ora } from '../../ora';
import { createOrModifyExpoConfigAsync, getPrivateExpoConfig } from '../../project/expoConfig';
import { findProjectIdByAccountNameAndSlugNullableAsync } from '../../project/fetchOrCreateProjectIDForWriteToConfigWithConfirmationAsync';
import { toAppPrivacy } from '../../project/projectUtils';
import { Choice, confirmAsync, promptAsync } from '../../prompts';
import { confirmAsync, promptAsync } from '../../prompts';
import { Actor } from '../../user/User';
type InitializeMethodOptions = {
@@ -234,11 +234,21 @@ export default class ProjectInit extends EasCommand {
if (allAccounts.length === 1) {
accountName = allAccounts[0].name;
} else {
const choices = ProjectInit.getAccountChoices(
actor,
accountNamesWhereUserHasSufficientPermissionsToCreateApp
);
// if regular user, put primary account first
const sortedAccounts =
actor.__typename === 'Robot'
? allAccounts
: [...allAccounts].sort((a, _b) =>
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
);
const choices = sortedAccounts.map(account => ({
title: account.name,
value: account,
description: !accountNamesWhereUserHasSufficientPermissionsToCreateApp.has(account.name)
? '(Viewer Role)'
: undefined,
}));
accountName = (
await promptAsync({
type: 'select',
@@ -311,71 +321,6 @@ export default class ProjectInit extends EasCommand {
return createdProjectId;
}
private static getAccountChoices(
actor: Actor,
namesWithSufficientPermissions: Set<string>
): Choice[] {
const allAccounts = actor.accounts;
const sortedAccounts =
actor.__typename === 'Robot'
? allAccounts
: [...allAccounts].sort((a, _b) =>
actor.__typename === 'User' ? (a.name === actor.username ? -1 : 1) : 0
);
if (actor.__typename !== 'Robot') {
const personalAccount = allAccounts?.find(
account => account?.ownerUserActor?.id === actor.id
);
const personalAccountChoice = personalAccount
? {
title: personalAccount.name,
value: personalAccount,
description: !namesWithSufficientPermissions.has(personalAccount.name)
? '(Personal) (Viewer Role)'
: '(Personal)',
}
: undefined;
const userAccounts = allAccounts
?.filter(account => account.ownerUserActor && account.name !== actor.username)
.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name)
? '(Team) (Viewer Role)'
: '(Team)',
}));
const organizationAccounts = allAccounts
?.filter(account => account.name !== actor.username && !account.ownerUserActor)
.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name)
? '(Organization) (Viewer Role)'
: '(Organization)',
}));
let choices: Choice[] = [];
if (personalAccountChoice) {
choices = [personalAccountChoice];
}
return [...choices, ...userAccounts, ...organizationAccounts].sort((a, _b) =>
actor.__typename === 'User' ? (a.value.name === actor.username ? -1 : 1) : 0
);
}
return sortedAccounts.map(account => ({
title: account.name,
value: account,
description: !namesWithSufficientPermissions.has(account.name) ? '(Viewer Role)' : undefined,
}));
}
async runAsync(): Promise<void> {
const {
flags: { id: idArgument, force, 'non-interactive': nonInteractive },
-5
View File
@@ -94,7 +94,6 @@ export default class Submit extends EasCommand {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.ProjectDir,
...this.ContextOptions.Analytics,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -103,7 +102,6 @@ export default class Submit extends EasCommand {
loggedIn: { actor, graphqlClient },
privateProjectConfig: { exp, projectId, projectDir },
analytics,
vcsClient,
} = await this.getContextAsync(Submit, {
nonInteractive: false,
});
@@ -120,7 +118,6 @@ export default class Submit extends EasCommand {
easJsonAccessor: EasJsonAccessor.fromProjectPath(projectDir),
platforms,
profileName: flagsWithPlatform.profile,
projectDir,
});
const submissions: SubmissionFragment[] = [];
@@ -137,8 +134,6 @@ export default class Submit extends EasCommand {
analytics,
exp,
projectId,
vcsClient,
specifiedProfile: flagsWithPlatform.profile,
});
if (submissionProfiles.length > 1) {
@@ -1,30 +0,0 @@
import { ExpoConfig } from '@expo/config';
import { instance, mock } from 'ts-mockito';
import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/createGraphqlClient';
import { ensureEASUpdateIsConfiguredAsync } from '../../../update/configure';
import { Client } from '../../../vcs/vcs';
describe(ensureEASUpdateIsConfiguredAsync, () => {
it('errors with "useClassicUpdates" set and no app.json', async () => {
const graphqlClient = instance(mock<ExpoGraphqlClient>({}));
const vcsClient = instance(mock<Client>({}));
const exp: ExpoConfig = {
name: 'test',
slug: 'test',
updates: { useClassicUpdates: true },
};
await expect(async () => {
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId: 'test',
projectDir: '/tmp/test',
platform: null,
vcsClient,
});
}).rejects.toThrow(
`Your app config sets "updates.useClassicUpdates" but EAS Update does not support classic updates. Remove "useClassicUpdates" from your app config and run this command again.`
);
});
});
@@ -1,5 +1,5 @@
import { AppJSONConfig, ExpoConfig, PackageJSONConfig, getConfig } from '@expo/config';
import { DirectoryJSON, vol } from 'memfs';
import { AppJSONConfig, PackageJSONConfig, getConfig } from '@expo/config';
import { vol } from 'memfs';
import { instance, mock } from 'ts-mockito';
import LoggedInContextField from '../../../commandUtils/context/LoggedInContextField';
@@ -8,17 +8,12 @@ import { ExpoGraphqlClient } from '../../../commandUtils/context/contextUtils/cr
import FeatureGateEnvOverrides from '../../../commandUtils/gating/FeatureGateEnvOverrides';
import FeatureGating from '../../../commandUtils/gating/FeatureGating';
import { jester } from '../../../credentials/__tests__/fixtures-constants';
import { UpdateFragment } from '../../../graphql/generated';
import { CodeSigningInfo, UpdateFragment } from '../../../graphql/generated';
import { PublishMutation } from '../../../graphql/mutations/PublishMutation';
import { AppQuery } from '../../../graphql/queries/AppQuery';
import { UpdateQuery } from '../../../graphql/queries/UpdateQuery';
import { getBranchNameFromChannelNameAsync } from '../../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../../update/queries';
import {
getCodeSigningInfoAsync,
getManifestBodyAsync,
signBody,
} from '../../../utils/code-signing';
import UpdateRepublish from '../republish';
const projectRoot = '/test-project';
@@ -38,6 +33,12 @@ const updateStub: UpdateFragment = {
createdAt: '2022-01-01T12:00:00Z',
};
const codeSigningStub: CodeSigningInfo = {
keyid: 'keyid',
alg: 'alg',
sig: 'sig',
};
jest.mock('fs');
jest.mock('@expo/config');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
@@ -51,12 +52,16 @@ jest.mock('../../../ora', () => ({
start: () => ({ succeed: () => {}, fail: () => {} }),
}),
}));
jest.mock('../../../utils/code-signing');
jest.mock('../../../fetch');
describe(UpdateRepublish.name, () => {
afterEach(() => vol.reset());
it('errors without --channel, --branch, or --group', async () => {
await expect(new UpdateRepublish([], commandOptions).run()).rejects.toThrow(
'--channel, --branch, or --group must be specified'
);
});
it('errors when providing both --group and --branch', async () => {
const flags = ['--group=1234', '--branch=main'];
@@ -151,45 +156,18 @@ describe(UpdateRepublish.name, () => {
});
it('re-creates update using codesigning with --group and --message', async () => {
const flags = [
'--group=1234',
'--message=test-republish',
`--private-key-path=./keys/test-private-key.pem`,
];
const flags = ['--group=1234', '--message=test-republish'];
const codeSigning = {
alg: 'rsa-v1_5-sha256',
alg: 'alg',
keyid: 'keyid',
sig: 'sig',
};
mockTestProject({
extraManifest: {
updates: {
codeSigningCertificate: './keys/test-certificate.pem',
codeSigningMetadata: {
alg: 'rsa-v1_5-sha256',
keyid: 'keyid',
},
},
},
extraVol: {
'./keys/test-private-key.pem': 'testpemprivate',
'./keys/test-certificate.pem': 'testpemcertificate',
},
});
jest.mocked(getCodeSigningInfoAsync).mockResolvedValue({
privateKey: '' as any,
certificate: '' as any,
codeSigningMetadata: { alg: 'rsa-v1_5-sha256', keyid: 'keyid' },
});
jest.mocked(getManifestBodyAsync).mockResolvedValue('test');
jest.mocked(signBody).mockReturnValue('sig');
mockTestProject();
// Mock queries to retrieve the update and code signing info
jest
.mocked(UpdateQuery.viewUpdateGroupAsync)
.mockResolvedValue([{ ...updateStub, codeSigningInfo: codeSigning }]);
.mockResolvedValue([{ ...updateStub, codeSigningInfo: codeSigningStub }]);
// Mock mutations to store the new update
jest.mocked(PublishMutation.publishUpdateGroupAsync).mockResolvedValue([
{
@@ -309,12 +287,8 @@ describe(UpdateRepublish.name, () => {
/** Create a new in-memory project, based on src/commands/project/__tests__/init.test.ts */
function mockTestProject({
configuredProjectId = '1234',
extraManifest,
extraVol,
}: {
configuredProjectId?: string;
extraManifest?: Partial<ExpoConfig>;
extraVol?: DirectoryJSON;
} = {}): void {
const packageJSON: PackageJSONConfig = {
name: 'testing123',
@@ -335,7 +309,6 @@ function mockTestProject({
projectId: configuredProjectId,
},
},
...extraManifest,
},
};
@@ -343,7 +316,6 @@ function mockTestProject({
{
'package.json': JSON.stringify(packageJSON),
'app.json': JSON.stringify(appJSON),
...extraVol,
},
projectRoot
);
@@ -43,10 +43,6 @@ jest.mock('@expo/config');
jest.mock('@expo/config-plugins');
jest.mock('../../../branch/queries');
jest.mock('../../../commandUtils/context/contextUtils/getProjectIdAsync');
jest.mock('../../../project/projectUtils', () => ({
...jest.requireActual('../../../project/projectUtils'),
enforceRollBackToEmbeddedUpdateSupportAsync: jest.fn(),
}));
jest.mock('../../../update/configure');
jest.mock('../../../update/getBranchNameFromChannelNameAsync');
jest.mock('../../../graphql/mutations/PublishMutation');
@@ -10,6 +10,7 @@ import {
ensureEASUpdateIsConfiguredAsync,
ensureEASUpdateIsConfiguredInEasJsonAsync,
} from '../../update/configure';
import { getVcsClient } from '../../vcs';
export default class UpdateConfigure extends EasCommand {
static override description = 'configure the project to support EAS Update';
@@ -27,7 +28,6 @@ export default class UpdateConfigure extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.ProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -36,29 +36,27 @@ export default class UpdateConfigure extends EasCommand {
const {
privateProjectConfig: { projectId, exp, projectDir },
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(UpdateConfigure, {
nonInteractive: flags['non-interactive'],
});
Log.log(
'💡 The following process will configure your project to use EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
'💡 The following process will configure your project to run EAS Update. These changes only apply to your local project files and you can safely revert them at any time.'
);
await vcsClient.ensureRepoExistsAsync();
await getVcsClient().ensureRepoExistsAsync();
await ensureEASUpdateIsConfiguredAsync(graphqlClient, {
exp,
projectId,
projectDir,
platform,
vcsClient,
});
await ensureEASUpdateIsConfiguredInEasJsonAsync(projectDir);
Log.addNewLineIfNone();
Log.log(`🎉 Your app is configured to use EAS Update!`);
Log.log(`🎉 Your app is configured with EAS Update!`);
Log.newLine();
const easJsonExists = await easJsonExistsAsync(projectDir);
if (!easJsonExists) {
+15 -52
View File
@@ -35,7 +35,6 @@ import {
getRuntimeVersionObjectAsync,
getUpdateMessageForCommandAsync,
isUploadedAssetCountAboveWarningThreshold,
platformDisplayNames,
resolveInputDirectoryAsync,
uploadAssetsAsync,
} from '../../project/publish';
@@ -52,10 +51,10 @@ import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
type RawUpdateFlags = {
auto: boolean;
dev: boolean;
branch?: string;
channel?: string;
message?: string;
@@ -74,7 +73,6 @@ type RawUpdateFlags = {
type UpdateFlags = {
auto: boolean;
dev: boolean;
platform: ExpoCLIExportPlatformFlag;
branchName?: string;
channelName?: string;
@@ -135,10 +133,6 @@ export default class UpdatePublish extends EasCommand {
default: 'all',
required: false,
}),
dev: Flags.boolean({
description: 'Publish a development bundle',
default: false,
}),
auto: Flags.boolean({
description:
'Use the current git branch and commit message for the EAS branch and update message',
@@ -154,7 +148,6 @@ export default class UpdatePublish extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -164,7 +157,6 @@ export default class UpdatePublish extends EasCommand {
auto: autoFlag,
platform: platformFlag,
channelName: channelNameArg,
dev,
updateMessage: updateMessageArg,
inputDir,
skipBundler,
@@ -179,7 +171,6 @@ export default class UpdatePublish extends EasCommand {
getDynamicPublicProjectConfigAsync,
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(UpdatePublish, {
nonInteractive,
});
@@ -201,7 +192,6 @@ export default class UpdatePublish extends EasCommand {
platform: getRequestedPlatform(platformFlag),
projectDir,
projectId,
vcsClient,
});
const { exp } = await getDynamicPublicProjectConfigAsync();
@@ -210,7 +200,6 @@ export default class UpdatePublish extends EasCommand {
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
vcsClient,
projectId,
channelNameArg,
branchNameArg,
@@ -219,7 +208,7 @@ export default class UpdatePublish extends EasCommand {
paginatedQueryOptions,
});
const updateMessage = await getUpdateMessageForCommandAsync(vcsClient, {
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
@@ -230,7 +219,7 @@ export default class UpdatePublish extends EasCommand {
if (!skipBundler) {
const bundleSpinner = ora().start('Exporting...');
try {
await buildBundlesAsync({ projectDir, inputDir, dev, exp, platformFlag, clearCache });
await buildBundlesAsync({ projectDir, inputDir, exp, platformFlag, clearCache });
bundleSpinner.succeed('Exported bundle(s)');
} catch (e) {
bundleSpinner.fail('Export failed');
@@ -253,22 +242,20 @@ export default class UpdatePublish extends EasCommand {
realizedPlatforms = Object.keys(assets) as PublishPlatform[];
// Timeout mechanism:
// - Start with NO_ACTIVITY_TIMEOUT. 90 seconds is chosen because the cloud function that processes
// uploaded assets has a timeout of 60 seconds and uploading can take some time on a slow connection.
// - Each time one or more assets reports as ready, reset the timeout.
// - Each time an asset upload begins, reset the timeout. This includes retries.
// - Start upload. Internally, uploadAssetsAsync uploads them all first and then checks for successful
// - Start with 60 second timeout. 60 seconds is chosen because the cloud function that processes
// uploaded assets has a timeout of 60 seconds.
// - Each time one or more assets reports as ready, reset the timeout to 60 seconds.
// - Start upload. Internally, uploadAssetsAsync uploads them all and then checks for successful
// processing every (5 + n) seconds with a linear backoff of n + 1 second.
// - At the same time as upload is started, start timeout checker which checks every 1 second to see
// if timeout has been reached. When timeout expires, send a cancellation signal to currently running
// upload function call to instruct it to stop uploading or checking for successful processing.
const NO_ACTIVITY_TIMEOUT = 90 * 1000; // 90 seconds
let lastUploadedStorageKeys = new Set<string>();
let lastAssetUploadResults: {
asset: RawAsset & { storageKey: string };
finished: boolean;
}[] = [];
let timeAtWhichToTimeout = Date.now() + NO_ACTIVITY_TIMEOUT;
let timeAtWhichToTimeout = Date.now() + 60 * 1000; // sixty seconds from now
const cancelationToken = { isCanceledOrFinished: false };
const uploadResults = await Promise.race([
@@ -282,7 +269,7 @@ export default class UpdatePublish extends EasCommand {
assetUploadResults.filter(r => r.finished).map(r => r.asset.storageKey)
);
if (!areSetsEqual(currentUploadedStorageKeys, lastUploadedStorageKeys)) {
timeAtWhichToTimeout = Date.now() + NO_ACTIVITY_TIMEOUT; // reset timeout to NO_ACTIVITY_TIMEOUT
timeAtWhichToTimeout = Date.now() + 60 * 1000; // reset timeout to sixty seconds from now
lastUploadedStorageKeys = currentUploadedStorageKeys;
lastAssetUploadResults = assetUploadResults;
}
@@ -290,10 +277,6 @@ export default class UpdatePublish extends EasCommand {
const totalAssets = assetUploadResults.length;
const missingAssetCount = assetUploadResults.filter(a => !a.finished).length;
assetSpinner.text = `Uploading (${totalAssets - missingAssetCount}/${totalAssets})`;
},
() => {
// when an upload is retried, reset the timeout as we know this will now need more time
timeAtWhichToTimeout = Date.now() + NO_ACTIVITY_TIMEOUT; // reset timeout to NO_ACTIVITY_TIMEOUT
}
),
(async () => {
@@ -342,32 +325,12 @@ export default class UpdatePublish extends EasCommand {
for (const uploadedAssetPath of uploadResults.uniqueUploadedAssetPaths) {
Log.debug(chalk.dim(`- ${uploadedAssetPath}`));
}
const platformString = (Object.keys(assets) as PublishPlatform[])
.map(platform => {
const collectedAssetForPlatform = nullthrows(assets[platform]);
const totalAssetsForPlatform = collectedAssetForPlatform.assets.length + 1; // launch asset
const assetString = totalAssetsForPlatform === 1 ? 'asset' : 'assets';
return `${totalAssetsForPlatform} ${platformDisplayNames[platform]} ${assetString}`;
})
.join(', ');
Log.withInfo(
`${platformString} (maximum: ${assetLimitPerUpdateGroup} total per update). ${learnMore(
'https://expo.fyi/eas-update-asset-limits',
{ learnMoreMessage: 'Learn more about asset limits.' }
)}`
);
} catch (e) {
assetSpinner.fail('Failed to upload');
throw e;
}
const runtimeVersions = await getRuntimeVersionObjectAsync(
exp,
realizedPlatforms,
projectDir,
vcsClient
);
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
const runtimeToPlatformMapping =
getRuntimeToPlatformMappingFromRuntimeVersions(runtimeVersions);
@@ -381,11 +344,12 @@ export default class UpdatePublish extends EasCommand {
branchId,
channelName: branchName,
});
Log.withTick(
`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`
);
}
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
@@ -543,7 +507,7 @@ export default class UpdatePublish extends EasCommand {
private sanitizeFlags(flags: RawUpdateFlags): UpdateFlags {
const nonInteractive = flags['non-interactive'] ?? false;
const { auto, branch: branchName, channel: channelName, dev, message: updateMessage } = flags;
const { auto, branch: branchName, channel: channelName, message: updateMessage } = flags;
if (nonInteractive && !auto && !(updateMessage && (branchName || channelName))) {
Errors.error(
'--branch and --message, or --channel and --message are required when updating in non-interactive mode unless --auto is specified',
@@ -572,7 +536,6 @@ export default class UpdatePublish extends EasCommand {
auto,
branchName,
channelName,
dev,
updateMessage,
inputDir: flags['input-dir'],
skipBundler: flags['skip-bundler'],
+135 -98
View File
@@ -1,21 +1,23 @@
import { Platform } from '@expo/config';
import { Flags } from '@oclif/core';
import { selectBranchOnAppAsync } from '../../branch/queries';
import { selectChannelOnAppAsync } from '../../channel/queries';
import { getUpdateGroupUrl } from '../../build/utils/url';
import EasCommand from '../../commandUtils/EasCommand';
import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/createGraphqlClient';
import { EasNonInteractiveAndJsonFlags } from '../../commandUtils/flags';
import { getPaginatedQueryOptions } from '../../commandUtils/pagination';
import { Update } from '../../graphql/generated';
import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import { UpdateQuery } from '../../graphql/queries/UpdateQuery';
import Log from '../../log';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import { promptAsync } from '../../prompts';
import { getBranchNameFromChannelNameAsync } from '../../update/getBranchNameFromChannelNameAsync';
import { selectUpdateGroupOnBranchAsync } from '../../update/queries';
import { UpdateToRepublish, republishAsync } from '../../update/republish';
import { truncateString as truncateUpdateMessage } from '../../update/utils';
import { getCodeSigningInfoAsync } from '../../utils/code-signing';
import { enableJsonOutput } from '../../utils/json';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
const defaultRepublishPlatforms: Platform[] = ['android', 'ios'];
@@ -25,7 +27,6 @@ type UpdateRepublishRawFlags = {
group?: string;
message?: string;
platform: string;
'private-key-path'?: string;
'non-interactive': boolean;
json?: boolean;
};
@@ -36,21 +37,34 @@ type UpdateRepublishFlags = {
groupId?: string;
updateMessage?: string;
platform: Platform[];
privateKeyPath?: string;
nonInteractive: boolean;
json: boolean;
};
type UpdateToRepublish = {
groupId: string;
branchId: string;
branchName: string;
} & Pick<
Update,
| 'message'
| 'runtimeVersion'
| 'manifestFragment'
| 'platform'
| 'gitCommitHash'
| 'codeSigningInfo'
>;
export default class UpdateRepublish extends EasCommand {
static override description = 'roll back to an existing update';
static override flags = {
channel: Flags.string({
description: 'Channel name to select an update group to republish from',
description: 'Channel name to select an update to republish from',
exclusive: ['branch', 'group'],
}),
branch: Flags.string({
description: 'Branch name to select an update group to republish from',
description: 'Branch name to select an update to republish from',
exclusive: ['channel', 'group'],
}),
group: Flags.string({
@@ -59,7 +73,7 @@ export default class UpdateRepublish extends EasCommand {
}),
message: Flags.string({
char: 'm',
description: 'Short message describing the republished update group',
description: 'Short message describing the republished update',
required: false,
}),
platform: Flags.enum({
@@ -68,10 +82,6 @@ export default class UpdateRepublish extends EasCommand {
default: 'all',
required: false,
}),
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
...EasNonInteractiveAndJsonFlags,
};
@@ -95,8 +105,6 @@ export default class UpdateRepublish extends EasCommand {
enableJsonOutput();
}
const codeSigningInfo = await getCodeSigningInfoAsync(exp, flags.privateKeyPath);
const existingUpdates = await getOrAskUpdatesAsync(graphqlClient, projectId, flags);
const updatesToPublish = existingUpdates.filter(update =>
flags.platform.includes(update.platform as Platform)
@@ -116,43 +124,117 @@ export default class UpdateRepublish extends EasCommand {
}
if (rawFlags.platform === 'all') {
Log.withTick(`The republished update group will appear only on: ${rawFlags.platform}`);
Log.withTick(`The republished update will appear only on: ${rawFlags.platform}`);
} else {
const platformsFromUpdates = updatesToPublish.map(update => update.platform);
if (platformsFromUpdates.length < defaultRepublishPlatforms.length) {
Log.warn(`You are republishing an update group that wasn't published for all platforms.`);
Log.warn(`You are republishing an update that wasn't published for all platforms.`);
}
Log.withTick(
`The republished update group will appear on the same platforms it was originally published on: ${platformsFromUpdates.join(
`The republished update will appear on the same platforms it was originally published on: ${platformsFromUpdates.join(
', '
)}`
);
}
// This command only republishes a single update group
// The update group properties are the same for all updates
const { branchId, branchName, runtimeVersion } = updatesToPublish[0];
const updateMessage = await getOrAskUpdateMessageAsync(updatesToPublish, flags);
const arbitraryUpdate = updatesToPublish[0];
await republishAsync({
graphqlClient,
app: { exp, projectId },
updatesToPublish,
targetBranch: { branchId: arbitraryUpdate.branchId, branchName: arbitraryUpdate.branchName },
updateMessage,
codeSigningInfo,
json: flags.json,
});
// If codesigning was created for the original update, we need to add it to the republish
const shouldRepublishWithCodesigning = updatesToPublish.some(update => update.codeSigningInfo);
if (shouldRepublishWithCodesigning) {
Log.withTick(
`The republished update will be signed with the same codesigning as the original update.`
);
}
const publishIndicator = ora('Republishing...').start();
let updatesRepublished: Awaited<ReturnType<typeof PublishMutation.publishUpdateGroupAsync>>;
try {
updatesRepublished = await PublishMutation.publishUpdateGroupAsync(graphqlClient, [
{
branchId,
runtimeVersion,
message: updateMessage,
updateInfoGroup: Object.fromEntries(
updatesToPublish.map(update => [update.platform, JSON.parse(update.manifestFragment)])
),
gitCommitHash: updatesToPublish[0].gitCommitHash,
awaitingCodeSigningInfo: shouldRepublishWithCodesigning,
},
]);
if (shouldRepublishWithCodesigning) {
const codeSigningByPlatform = Object.fromEntries(
updatesToPublish.map(update => [update.platform, update.codeSigningInfo])
);
await Promise.all(
updatesRepublished.map(async update => {
const codeSigning = codeSigningByPlatform[update.platform];
if (codeSigning) {
await PublishMutation.setCodeSigningInfoAsync(graphqlClient, update.id, codeSigning);
}
})
);
}
publishIndicator.succeed('Republished update');
} catch (error: any) {
publishIndicator.fail('Failed to republish update');
throw error;
}
if (flags.json) {
return printJsonOnlyOutput(updatesRepublished);
}
const updatesRepublishedByPlatform = Object.fromEntries(
updatesRepublished.map(update => [update.platform, update])
);
const updateGroupUrl = getUpdateGroupUrl(
(await getOwnerAccountForProjectIdAsync(graphqlClient, projectId)).name,
exp.slug,
updatesRepublished[0].group
);
Log.addNewLineIfNone();
Log.log(
formatFields([
{ label: 'Branch', value: branchName },
{ label: 'Runtime version', value: updatesRepublished[0].runtimeVersion },
{ label: 'Platform', value: updatesRepublished.map(update => update.platform).join(', ') },
{ label: 'Update Group ID', value: updatesRepublished[0].id },
...(updatesRepublishedByPlatform.android
? [{ label: 'Android update ID', value: updatesRepublishedByPlatform.android.id }]
: []),
...(updatesRepublishedByPlatform.ios
? [{ label: 'iOS update ID', value: updatesRepublishedByPlatform.ios.id }]
: []),
{ label: 'Message', value: updateMessage },
{ label: 'Website link', value: link(updateGroupUrl, { dim: false }) },
])
);
}
private sanitizeFlags(rawFlags: UpdateRepublishRawFlags): UpdateRepublishFlags {
sanitizeFlags(rawFlags: UpdateRepublishRawFlags): UpdateRepublishFlags {
const branchName = rawFlags.branch;
const channelName = rawFlags.channel;
const groupId = rawFlags.group;
const nonInteractive = rawFlags['non-interactive'];
const privateKeyPath = rawFlags['private-key-path'];
if (nonInteractive && !groupId) {
throw new Error('Only --group can be used in non-interactive mode');
}
if (!groupId && !(branchName || channelName)) {
throw new Error(`--channel, --branch, or --group must be specified`);
}
const platform =
rawFlags.platform === 'all' ? defaultRepublishPlatforms : ([rawFlags.platform] as Platform[]);
@@ -163,7 +245,6 @@ export default class UpdateRepublish extends EasCommand {
groupId,
platform,
updateMessage: rawFlags.message,
privateKeyPath,
json: rawFlags.json ?? false,
nonInteractive,
};
@@ -177,22 +258,18 @@ async function getOrAskUpdatesAsync(
flags: UpdateRepublishFlags
): Promise<UpdateToRepublish[]> {
if (flags.groupId) {
const updateGroup = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
const updateGroups = await UpdateQuery.viewUpdateGroupAsync(graphqlClient, {
groupId: flags.groupId,
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
return updateGroups.map(group => ({
...group,
groupId: group.group,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
if (flags.nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
if (flags.branchName) {
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
@@ -209,55 +286,7 @@ async function getOrAskUpdatesAsync(
});
}
const { choice } = await promptAsync({
type: 'select',
message: 'Find update by branch or channel?',
name: 'choice',
choices: [
{ title: 'Branch', value: 'branch' },
{ title: 'Channel', value: 'channel' },
],
});
if (choice === 'channel') {
const { name } = await selectChannelOnAppAsync(graphqlClient, {
projectId,
selectionPromptTitle: 'Select a channel to view',
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromChannelNameAsync(graphqlClient, {
...flags,
channelName: name,
projectId,
});
} else if (choice === 'branch') {
const { name } = await selectBranchOnAppAsync(graphqlClient, {
projectId,
promptTitle: 'Select branch from which to choose update',
displayTextForListItem: updateBranch => ({
title: updateBranch.name,
}),
// discard limit and offset because this query is not their intended target
paginatedQueryOptions: {
json: flags.json,
nonInteractive: flags.nonInteractive,
offset: 0,
},
});
return await askUpdatesFromBranchNameAsync(graphqlClient, {
...flags,
branchName: name,
projectId,
});
} else {
throw new Error('Must choose update via channel or branch');
}
throw new Error('--channel, --branch, or --group is required');
}
/** Ask the user which update needs to be republished by branch name, this requires interactive mode */
@@ -270,17 +299,21 @@ async function askUpdatesFromBranchNameAsync(
nonInteractive,
}: { projectId: string; branchName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
const updateGroup = await selectUpdateGroupOnBranchAsync(graphqlClient, {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const updateGroups = await selectUpdateGroupOnBranchAsync(graphqlClient, {
projectId,
branchName,
paginatedQueryOptions: getPaginatedQueryOptions({ json, 'non-interactive': nonInteractive }),
});
return updateGroup.map(update => ({
...update,
groupId: update.group,
branchId: update.branch.id,
branchName: update.branch.name,
return updateGroups.map(group => ({
...group,
groupId: group.id,
branchId: group.branch.id,
branchName: group.branch.name,
}));
}
/** Ask the user which update needs to be republished by channel name, this requires interactive mode */
@@ -293,6 +326,10 @@ async function askUpdatesFromChannelNameAsync(
nonInteractive,
}: { projectId: string; channelName: string; json: boolean; nonInteractive: boolean }
): Promise<UpdateToRepublish[]> {
if (nonInteractive) {
throw new Error('Must supply --group when in non-interactive mode');
}
const branchName = await getBranchNameFromChannelNameAsync(graphqlClient, projectId, channelName);
return await askUpdatesFromBranchNameAsync(graphqlClient, {
@@ -20,10 +20,7 @@ import { PublishMutation } from '../../graphql/mutations/PublishMutation';
import Log, { link } from '../../log';
import { ora } from '../../ora';
import { RequestedPlatform } from '../../platform';
import {
enforceRollBackToEmbeddedUpdateSupportAsync,
getOwnerAccountForProjectIdAsync,
} from '../../project/projectUtils';
import { getOwnerAccountForProjectIdAsync } from '../../project/projectUtils';
import {
ExpoCLIExportPlatformFlag,
defaultPublishPlatforms,
@@ -46,6 +43,7 @@ import uniqBy from '../../utils/expodash/uniqBy';
import formatFields from '../../utils/formatFields';
import { enableJsonOutput, printJsonOnlyOutput } from '../../utils/json';
import { maybeWarnAboutEasOutagesAsync } from '../../utils/statuspageService';
import { getVcsClient } from '../../vcs';
type RawUpdateFlags = {
auto: boolean;
@@ -70,6 +68,7 @@ type UpdateFlags = {
};
export default class UpdateRollBackToEmbedded extends EasCommand {
static override hidden = true; // until we launch
static override description = 'roll back to the embedded update';
static override flags = {
@@ -87,11 +86,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
}),
platform: Flags.enum({
char: 'p',
options: [
// TODO: Add web when it's fully supported
...defaultPublishPlatforms,
'all',
],
options: [...defaultPublishPlatforms, 'all'],
default: 'all',
required: false,
}),
@@ -110,7 +105,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
static override contextDefinition = {
...this.ContextOptions.DynamicProjectConfig,
...this.ContextOptions.LoggedIn,
...this.ContextOptions.Vcs,
};
async runAsync(): Promise<void> {
@@ -131,7 +125,6 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
getDynamicPublicProjectConfigAsync,
getDynamicPrivateProjectConfigAsync,
loggedIn: { graphqlClient },
vcsClient,
} = await this.getContextAsync(UpdateRollBackToEmbedded, {
nonInteractive,
});
@@ -153,19 +146,14 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
platform: getRequestedPlatform(platformFlag),
projectDir,
projectId,
vcsClient,
});
// check that the expo-updates package version supports roll back to embedded
await enforceRollBackToEmbeddedUpdateSupportAsync(projectDir);
const { exp } = await getDynamicPublicProjectConfigAsync();
const { exp: expPrivate } = await getDynamicPrivateProjectConfigAsync();
const codeSigningInfo = await getCodeSigningInfoAsync(expPrivate, privateKeyPath);
const branchName = await getBranchNameForCommandAsync({
graphqlClient,
vcsClient,
projectId,
channelNameArg,
branchNameArg,
@@ -174,7 +162,7 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
paginatedQueryOptions,
});
const updateMessage = await getUpdateMessageForCommandAsync(vcsClient, {
const updateMessage = await getUpdateMessageForCommandAsync({
updateMessageArg,
autoFlag,
nonInteractive,
@@ -198,15 +186,12 @@ export default class UpdateRollBackToEmbedded extends EasCommand {
Log.withTick(`Channel: ${chalk.bold(branchName)} pointed at branch: ${chalk.bold(branchName)}`);
const vcsClient = getVcsClient();
const gitCommitHash = await vcsClient.getCommitHashAsync();
const isGitWorkingTreeDirty = await vcsClient.hasUncommittedChangesAsync();
const runtimeVersions = await getRuntimeVersionObjectAsync(
exp,
realizedPlatforms,
projectDir,
vcsClient
);
const runtimeVersions = await getRuntimeVersionObjectAsync(exp, realizedPlatforms, projectDir);
let newUpdates: UpdatePublishMutation['updateBranch']['publishUpdateGroups'];
const publishSpinner = ora('Publishing...').start();
@@ -1,40 +0,0 @@
import { Flags } from '@oclif/core';
import EasCommand from '../../commandUtils/EasCommand';
import { promptAsync } from '../../prompts';
import UpdateRepublish from './republish';
import UpdateRollBackToEmbedded from './roll-back-to-embedded';
export default class UpdateRollback extends EasCommand {
static override description = 'roll back to an embedded update or an existing update';
static override flags = {
'private-key-path': Flags.string({
description: `File containing the PEM-encoded private key corresponding to the certificate in expo-updates' configuration. Defaults to a file named "private-key.pem" in the certificate's directory.`,
required: false,
}),
};
async runAsync(): Promise<void> {
const { flags } = await this.parse(UpdateRollback);
const { choice } = await promptAsync({
type: 'select',
message: 'Which type of update would you like to roll back to?',
name: 'choice',
choices: [
{ title: 'Published Update', value: 'published' },
{ title: 'Embedded Update', value: 'embedded' },
],
});
const privateKeyPathArg = flags['private-key-path']
? ['--private-key-path', flags['private-key-path']]
: [];
if (choice === 'published') {
await UpdateRepublish.run(privateKeyPathArg);
} else {
await UpdateRollBackToEmbedded.run(privateKeyPathArg);
}
}
}
@@ -11,7 +11,6 @@ import {
} from '../../../project/projectUtils';
import { promptAsync } from '../../../prompts';
import { CredentialsContext } from '../../context';
import { AndroidPackageNotDefinedError } from '../../errors';
import { AppLookupParams } from '../api/GraphqlClient';
/**
@@ -109,11 +108,10 @@ export async function getAppLookupParamsFromContextAsync(
const androidApplicationIdentifier = await getApplicationIdAsync(
ctx.projectDir,
ctx.exp,
ctx.vcsClient,
gradleContext
);
if (!androidApplicationIdentifier) {
throw new AndroidPackageNotDefinedError(
throw new Error(
`android.package needs to be defined in your ${getProjectConfigDescription(
ctx.projectDir
)} file`
@@ -260,6 +260,7 @@ export async function createKeystoreAsync(
keystorePassword: keystore.keystorePassword,
keyAlias: keystore.keyAlias,
keyPassword: keystore.keyPassword,
type: keystore.type,
},
account.id
);
@@ -5,10 +5,10 @@ import gql from 'graphql-tag';
import { ExpoGraphqlClient } from '../../../../../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../../../../../graphql/client';
import {
AndroidAppBuildCredentials,
AndroidAppBuildCredentialsFragment,
AndroidAppBuildCredentialsInput,
CreateAndroidAppBuildCredentialsMutation,
SetDefaultAndroidAppBuildCredentialsMutation,
SetKeystoreMutation,
} from '../../../../../graphql/generated';
import { AndroidAppBuildCredentialsFragmentNode } from '../../../../../graphql/types/credentials/AndroidAppBuildCredentials';
@@ -62,9 +62,9 @@ export const AndroidAppBuildCredentialsMutation = {
): Promise<AndroidAppBuildCredentialsFragment> {
const data = await withErrorHandlingAsync(
graphqlClient
.mutation<SetDefaultAndroidAppBuildCredentialsMutation>(
.mutation<AndroidAppBuildCredentials>(
gql`
mutation SetDefaultAndroidAppBuildCredentialsMutation(
mutation AndroidAppBuildCredentialsMutation(
$androidAppBuildCredentialsId: ID!
$isDefault: Boolean!
) {
@@ -85,7 +85,7 @@ export const AndroidAppBuildCredentialsMutation = {
.toPromise()
);
assert(data, `GraphQL: 'setDefault' not defined in server response ${JSON.stringify(data)}}`);
return data.androidAppBuildCredentials.setDefault;
return data;
},
async setKeystoreAsync(
graphqlClient: ExpoGraphqlClient,
@@ -9,7 +9,6 @@ import Log from '../log';
import { getPrivateExpoConfig } from '../project/expoConfig';
import { confirmAsync } from '../prompts';
import { Actor } from '../user/User';
import { Client } from '../vcs/vcs';
import * as AndroidGraphqlClient from './android/api/GraphqlClient';
import * as IosGraphqlClient from './ios/api/GraphqlClient';
import AppStoreApi from './ios/appstore/AppStoreApi';
@@ -29,7 +28,6 @@ export class CredentialsContext {
public readonly user: Actor;
public readonly graphqlClient: ExpoGraphqlClient;
public readonly analytics: Analytics;
public readonly vcsClient: Client;
public readonly easJsonCliConfig?: EasJson['cli'];
private shouldAskAuthenticateAppStore: boolean = true;
@@ -46,7 +44,6 @@ export class CredentialsContext {
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
env?: Env;
}
) {
@@ -55,7 +52,6 @@ export class CredentialsContext {
this.user = options.user;
this.graphqlClient = options.graphqlClient;
this.analytics = options.analytics;
this.vcsClient = options.vcsClient;
this.nonInteractive = options.nonInteractive ?? false;
this.projectInfo = options.projectInfo;
}
@@ -6,8 +6,8 @@ import { AndroidAppBuildCredentialsFragment, IosDistributionType } from '../../g
import Log from '../../log';
import { findApplicationTarget, findTargetByName } from '../../project/ios/target';
import zipObject from '../../utils/expodash/zipObject';
import { getVcsClient } from '../../vcs';
import GitClient from '../../vcs/clients/git';
import { Client } from '../../vcs/vcs';
import { CredentialsContext } from '../context';
import { App, Target, TargetCredentials } from '../ios/types';
import { readRawAsync } from './read';
@@ -38,7 +38,7 @@ export async function updateAndroidCredentialsAsync(
rawCredentialsJson?.android?.keystore?.keystorePath ?? 'credentials/android/keystore.jks';
Log.log(`Writing Keystore to ${keystorePath}`);
await updateFileAsync(ctx.projectDir, keystorePath, keystore.keystore);
const shouldWarnKeystore = await isFileUntrackedAsync(keystorePath, ctx.vcsClient);
const shouldWarnKeystore = await isFileUntrackedAsync(keystorePath);
const androidCredentials: Partial<CredentialsJson['android']> = {
keystore: {
@@ -52,7 +52,7 @@ export async function updateAndroidCredentialsAsync(
await fs.writeJson(getCredentialsJsonPath(ctx.projectDir), rawCredentialsJson, {
spaces: 2,
});
const shouldWarnCredentialsJson = await isFileUntrackedAsync('credentials.json', ctx.vcsClient);
const shouldWarnCredentialsJson = await isFileUntrackedAsync('credentials.json');
const newFilePaths = [];
if (shouldWarnKeystore) {
@@ -137,14 +137,14 @@ export async function updateIosCredentialsAsync(
const newFilePaths = [];
for (const [, targetCredentials] of Object.entries(iosCredentials)) {
if (await isFileUntrackedAsync(targetCredentials.distributionCertificate.path, ctx.vcsClient)) {
if (await isFileUntrackedAsync(targetCredentials.distributionCertificate.path)) {
newFilePaths.push(targetCredentials.distributionCertificate.path);
}
if (await isFileUntrackedAsync(targetCredentials.provisioningProfilePath, ctx.vcsClient)) {
if (await isFileUntrackedAsync(targetCredentials.provisioningProfilePath)) {
newFilePaths.push(targetCredentials.provisioningProfilePath);
}
}
if (await isFileUntrackedAsync('credentials.json', ctx.vcsClient)) {
if (await isFileUntrackedAsync('credentials.json')) {
newFilePaths.push('credentials.json');
}
displayUntrackedFilesWarning(newFilePaths);
@@ -296,7 +296,8 @@ async function updateFileAsync(
}
}
async function isFileUntrackedAsync(path: string, vcsClient: Client): Promise<boolean> {
async function isFileUntrackedAsync(path: string): Promise<boolean> {
const vcsClient = getVcsClient();
if (vcsClient instanceof GitClient) {
return await vcsClient.isFileUntrackedAsync(path);
}
@@ -15,9 +15,3 @@ export class UnsupportedCredentialsChoiceError extends Error {
super(message);
}
}
export class AndroidPackageNotDefinedError extends Error {
constructor(message?: string) {
super(message ?? 'android.package needs to be defined in your app.json/app.config.js file');
}
}
@@ -31,7 +31,7 @@ export function formatDeviceLabel(device: AppleDeviceFragment): string {
const deviceDetails = formatDeviceDetails(device);
return `${device.identifier}${deviceDetails !== '' ? ` ${deviceDetails}` : ''}${
device.name ? ` (${device.name})` : ''
}${device.createdAt ? ` (created at: ${device.createdAt})` : ''}`;
}`;
}
function formatDeviceDetails(device: AppleDeviceFragment): string {
@@ -5,8 +5,6 @@ import nullthrows from 'nullthrows';
import DeviceCreateAction, { RegistrationMethod } from '../../../devices/actions/create/action';
import {
AppleAppIdentifierFragment,
AppleDevice,
AppleDeviceFragment,
AppleDistributionCertificateFragment,
AppleProvisioningProfileFragment,
@@ -21,7 +19,6 @@ import differenceBy from '../../../utils/expodash/differenceBy';
import { CredentialsContext } from '../../context';
import { MissingCredentialsNonInteractiveError } from '../../errors';
import { AppLookupParams } from '../api/graphql/types/AppLookupParams';
import { ProvisioningProfile } from '../appstore/Credentials.types';
import { ApplePlatform } from '../appstore/constants';
import { Target } from '../types';
import { validateProvisioningProfileAsync } from '../validators/validateProvisioningProfile';
@@ -149,14 +146,25 @@ export class SetUpAdhocProvisioningProfile {
);
let appleProvisioningProfile: AppleProvisioningProfileFragment | null = null;
if (currentBuildCredentials?.provisioningProfile) {
appleProvisioningProfile = await this.reuseCurrentProvisioningProfileAsync(
currentBuildCredentials.provisioningProfile,
provisioningProfileStoreInfo,
ctx,
app,
appleAppIdentifier,
chosenDevices
);
if (
currentBuildCredentials.provisioningProfile.developerPortalIdentifier !==
provisioningProfileStoreInfo.provisioningProfileId
) {
await ctx.ios.deleteProvisioningProfilesAsync(ctx.graphqlClient, [
currentBuildCredentials.provisioningProfile.id,
]);
appleProvisioningProfile = await ctx.ios.createProvisioningProfileAsync(
ctx.graphqlClient,
app,
appleAppIdentifier,
{
appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile,
developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId,
}
);
} else {
appleProvisioningProfile = currentBuildCredentials.provisioningProfile;
}
} else {
appleProvisioningProfile = await ctx.ios.createProvisioningProfileAsync(
ctx.graphqlClient,
@@ -169,23 +177,7 @@ export class SetUpAdhocProvisioningProfile {
);
}
// 6. Compare selected devices with the ones actually provisioned
const diffList = differenceBy(
chosenDevices,
appleProvisioningProfile.appleDevices,
'identifier'
);
if (diffList.length > 0) {
Log.warn(`Failed to provision ${diffList.length} of the selected devices:`);
for (const missingDevice of diffList) {
Log.warn(`- ${formatDeviceLabel(missingDevice)}`);
}
Log.log(
'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet'
);
}
// 7. Create (or update) app build credentials
// 6. Create (or update) app build credentials
assert(appleProvisioningProfile);
return await assignBuildCredentialsAsync(
ctx,
@@ -197,49 +189,6 @@ export class SetUpAdhocProvisioningProfile {
);
}
private async reuseCurrentProvisioningProfileAsync(
currentProvisioningProfile: AppleProvisioningProfileFragment,
provisioningProfileStoreInfo: ProvisioningProfile,
ctx: CredentialsContext,
app: AppLookupParams,
appleAppIdentifier: AppleAppIdentifierFragment,
chosenDevices: AppleDevice[]
): Promise<AppleProvisioningProfileFragment> {
if (
currentProvisioningProfile.developerPortalIdentifier !==
provisioningProfileStoreInfo.provisioningProfileId
) {
// If IDs don't match, the profile needs to be deleted and re-created
await ctx.ios.deleteProvisioningProfilesAsync(ctx.graphqlClient, [
currentProvisioningProfile.id,
]);
return await ctx.ios.createProvisioningProfileAsync(
ctx.graphqlClient,
app,
appleAppIdentifier,
{
appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile,
developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId,
}
);
} else if (
differenceBy(chosenDevices, currentProvisioningProfile.appleDevices, 'identifier').length > 0
) {
// If IDs match, but the devices lists don't, the profile needs to be updated first
return await ctx.ios.updateProvisioningProfileAsync(
ctx.graphqlClient,
currentProvisioningProfile.id,
{
appleProvisioningProfile: provisioningProfileStoreInfo.provisioningProfile,
developerPortalIdentifier: provisioningProfileStoreInfo.provisioningProfileId,
}
);
} else {
// Otherwise the current profile can be reused
return currentProvisioningProfile;
}
}
private async areBuildCredentialsSetupAsync(ctx: CredentialsContext): Promise<boolean> {
const { app, target } = this.options;
const buildCredentials = await getBuildCredentialsAsync(ctx, app, IosDistributionType.AdHoc);
@@ -1,25 +0,0 @@
import { v4 as uuidv4 } from 'uuid';
import { AppleDeviceClass, AppleDeviceFragment } from '../../../../graphql/generated';
import { formatDeviceLabel } from '../DeviceUtils';
describe(formatDeviceLabel, () => {
it('returns createdAt clause', async () => {
const currentDate = new Date();
const appleDevice = {
__typename: 'AppleDevice',
id: uuidv4(),
identifier: 'test-apple-device-id',
name: 'test-apple-device-name',
model: '15',
deviceClass: AppleDeviceClass.Iphone,
createdAt: currentDate.toISOString(),
} as AppleDeviceFragment;
const result = formatDeviceLabel(appleDevice);
expect(result).toEqual(
`test-apple-device-id (iPhone 15) (test-apple-device-name) (created at: ${currentDate.toISOString()})`
);
});
});
@@ -1,39 +1,4 @@
import { Env } from '@expo/eas-build-job';
import { EasJson } from '@expo/eas-json';
import { Analytics } from '../../../../analytics/AnalyticsManager';
import { ExpoGraphqlClient } from '../../../../commandUtils/context/contextUtils/createGraphqlClient';
import {
Account,
AppleAppIdentifierFragment,
AppleDevice,
AppleDeviceClass,
AppleDeviceFragment,
AppleDistributionCertificateFragment,
IosAppBuildCredentialsFragment,
} from '../../../../graphql/generated';
import Log from '../../../../log';
import { getApplePlatformFromTarget } from '../../../../project/ios/target';
import { Actor } from '../../../../user/User';
import { Client } from '../../../../vcs/vcs';
import { CredentialsContext, CredentialsContextProjectInfo } from '../../../context';
import { ProvisioningProfile } from '../../appstore/Credentials.types';
import { ApplePlatform } from '../../appstore/constants';
import { assignBuildCredentialsAsync, getBuildCredentialsAsync } from '../BuildCredentialsUtils';
import { chooseDevicesAsync } from '../DeviceUtils';
import { SetUpAdhocProvisioningProfile, doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
jest.mock('../BuildCredentialsUtils');
jest.mock('../../../context');
jest.mock('../../../ios/api/GraphqlClient');
jest.mock('../DeviceUtils', () => {
return {
__esModule: true,
chooseDevicesAsync: jest.fn(),
formatDeviceLabel: jest.requireActual('../DeviceUtils').formatDeviceLabel,
};
});
jest.mock('../../../../project/ios/target');
import { doUDIDsMatch } from '../SetUpAdhocProvisioningProfile';
describe(doUDIDsMatch, () => {
it('return false if UDIDs do not match', () => {
@@ -47,138 +12,3 @@ describe(doUDIDsMatch, () => {
expect(doUDIDsMatch(udidsA, udidsB)).toBe(true);
});
});
describe('runWithDistributionCertificateAsync', () => {
beforeEach(() => {
jest.resetAllMocks();
});
const setUpAdhocProvisioningProfile = new SetUpAdhocProvisioningProfile({
app: { account: {} as Account, projectName: 'projName', bundleIdentifier: 'bundleId' },
target: { targetName: 'targetName', bundleIdentifier: 'bundleId', entitlements: {} },
});
describe('compare chosen and provisioned devices', () => {
describe('not all devices provisioned', () => {
describe('still not provisioned after an update', () => {
it('displays warning to the user and lists the missing devices', async () => {
const { ctx, distCert } = setUpTest();
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
provisioningProfile: {
appleTeam: {},
appleDevices: [{ identifier: 'id1' }],
developerPortalIdentifier: 'provisioningProfileId',
},
} as IosAppBuildCredentialsFragment);
ctx.ios.updateProvisioningProfileAsync = jest.fn().mockResolvedValue({
appleTeam: {},
appleDevices: [{ identifier: 'id1' }],
developerPortalIdentifier: 'provisioningProfileId',
});
const LogWarnSpy = jest.spyOn(Log, 'warn');
const LogLogSpy = jest.spyOn(Log, 'log');
const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(
ctx,
distCert
);
expect(result).toEqual({} as IosAppBuildCredentialsFragment);
expect(LogWarnSpy).toHaveBeenCalledTimes(3);
expect(LogWarnSpy).toHaveBeenCalledWith('Failed to provision 2 of the selected devices:');
expect(LogWarnSpy).toHaveBeenCalledWith('- id2 (iPhone) (Device 2)');
expect(LogWarnSpy).toHaveBeenCalledWith('- id3 (Mac) (Device 3)');
expect(LogLogSpy).toHaveBeenCalledTimes(1);
expect(LogLogSpy).toHaveBeenCalledWith(
'Most commonly devices fail to to be provisioned while they are still being processed by Apple, which can take up to 24-72 hours. Check your Apple Developer Portal page at https://developer.apple.com/account/resources/devices/list, the devices in "Processing" status cannot be provisioned yet'
);
});
});
describe('all devices provisioned after an update', () => {
it('does not display warning', async () => {
const { ctx, distCert } = setUpTest();
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
provisioningProfile: {
appleTeam: {},
appleDevices: [{ identifier: 'id1' }],
developerPortalIdentifier: 'provisioningProfileId',
},
} as IosAppBuildCredentialsFragment);
ctx.ios.updateProvisioningProfileAsync = jest.fn().mockResolvedValue({
appleTeam: {},
appleDevices: [{ identifier: 'id1' }, { identifier: 'id2' }, { identifier: 'id3' }],
developerPortalIdentifier: 'provisioningProfileId',
});
const LogWarnSpy = jest.spyOn(Log, 'warn');
const LogLogSpy = jest.spyOn(Log, 'log');
const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(
ctx,
distCert
);
expect(result).toEqual({} as IosAppBuildCredentialsFragment);
expect(LogWarnSpy).not.toHaveBeenCalled();
expect(LogLogSpy).not.toHaveBeenCalled();
});
});
});
describe('all devices provisioned', () => {
it('does not display warning', async () => {
const { ctx, distCert } = setUpTest();
jest.mocked(getBuildCredentialsAsync).mockResolvedValue({
provisioningProfile: {
appleTeam: {},
appleDevices: [{ identifier: 'id1' }, { identifier: 'id2' }, { identifier: 'id3' }],
developerPortalIdentifier: 'provisioningProfileId',
},
} as IosAppBuildCredentialsFragment);
const LogWarnSpy = jest.spyOn(Log, 'warn');
const LogLogSpy = jest.spyOn(Log, 'log');
const result = await setUpAdhocProvisioningProfile.runWithDistributionCertificateAsync(
ctx,
distCert
);
expect(result).toEqual({} as IosAppBuildCredentialsFragment);
expect(LogWarnSpy).not.toHaveBeenCalled();
expect(LogLogSpy).not.toHaveBeenCalled();
});
});
});
});
function setUpTest(): { ctx: CredentialsContext; distCert: AppleDistributionCertificateFragment } {
const ctx = jest.mocked(
new CredentialsContext(
{} as {
projectInfo: CredentialsContextProjectInfo | null;
easJsonCliConfig?: EasJson['cli'];
nonInteractive: boolean;
projectDir: string;
user: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
env?: Env;
vcsClient: Client;
}
)
);
Object.defineProperty(ctx, 'ios', { value: jest.mock('../../../ios/api/GraphqlClient') });
ctx.ios.getDevicesForAppleTeamAsync = jest
.fn()
.mockResolvedValue([
{ identifier: 'id1' },
{ identifier: 'id2' },
{ identifier: 'id3' },
] as AppleDeviceFragment[]);
jest.mocked(chooseDevicesAsync).mockResolvedValue([
{ identifier: 'id1', name: 'Device 1', deviceClass: AppleDeviceClass.Ipad },
{ identifier: 'id2', name: 'Device 2', deviceClass: AppleDeviceClass.Iphone },
{ identifier: 'id3', name: 'Device 3', deviceClass: AppleDeviceClass.Mac },
] as AppleDevice[]);
// @ts-ignore
jest.mocked(getApplePlatformFromTarget).mockResolvedValue(ApplePlatform.IOS);
Object.defineProperty(ctx, 'appStore', { value: jest.mock('../../appstore/AppStoreApi') });
ctx.appStore.createOrReuseAdhocProvisioningProfileAsync = jest.fn().mockResolvedValue({
provisioningProfileId: 'provisioningProfileId',
} as ProvisioningProfile);
ctx.ios.createOrGetExistingAppleAppIdentifierAsync = jest
.fn()
.mockResolvedValue({} as AppleAppIdentifierFragment);
jest.mocked(assignBuildCredentialsAsync).mockResolvedValue({} as IosAppBuildCredentialsFragment);
const distCert = {} as AppleDistributionCertificateFragment;
return { ctx, distCert };
}
@@ -8,7 +8,6 @@ import {
} from '@expo/apple-utils';
import assert from 'assert';
import chalk from 'chalk';
import { int } from 'getenv';
import Log from '../../../log';
import { toggleConfirmAsync } from '../../../prompts';
@@ -189,10 +188,7 @@ async function authenticateAsUserAsync(options: Options = {}): Promise<AuthCtx>
const authState = await loginAsync(
{
cookies: options.cookies,
teamId: options.teamId ?? process.env.EXPO_APPLE_TEAM_ID,
providerId: process.env.EXPO_APPLE_PROVIDER_ID
? int(process.env.EXPO_APPLE_PROVIDER_ID)
: undefined,
teamId: options.teamId,
},
{
// TODO: Provide a way to disable this for users who want to mix and match teams / providers.
@@ -733,13 +733,6 @@ export const CapabilityMapping: CapabilityClassifier[] = [
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
entitlement: 'com.apple.developer.proximity-reader.payment.acceptance',
name: 'Tap to Pay on iPhone',
capability: CapabilityType.TAP_TO_PAY_ON_IPHONE,
validateOptions: validateBooleanOptions,
getOptions: getBooleanOptions,
},
{
entitlement: 'com.apple.developer.matter.allow-setup-payload',
name: 'Matter Allow Setup Payload',
@@ -4,14 +4,12 @@ import { ExpoGraphqlClient } from '../../commandUtils/context/contextUtils/creat
import Log from '../../log';
import { pressAnyKeyToContinueAsync } from '../../prompts';
import { Actor } from '../../user/User';
import { Client } from '../../vcs/vcs';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
export interface Action<T = void> {
actor: Actor;
graphqlClient: ExpoGraphqlClient;
analytics: Analytics;
vcsClient: Client;
projectInfo: CredentialsContextProjectInfo | null;
getDynamicPrivateProjectConfigAsync: DynamicConfigContextFn;
runAsync(ctx: CredentialsContext): Promise<T>;
@@ -27,7 +27,6 @@ import {
displayEmptyAndroidCredentials,
} from '../android/utils/printCredentials';
import { CredentialsContext, CredentialsContextProjectInfo } from '../context';
import { AndroidPackageNotDefinedError } from '../errors';
import { ActionInfo, AndroidActionType, Scope } from './Actions';
import {
buildCredentialsActions,
@@ -65,7 +64,6 @@ export class ManageAndroid {
analytics: this.callingAction.analytics,
env: buildProfile?.env,
nonInteractive: false,
vcsClient: this.callingAction.vcsClient,
});
let gradleContext;
@@ -133,12 +131,6 @@ export class ManageAndroid {
await this.runProjectSpecificActionAsync(ctx, chosenAction, gradleContext);
} catch (err) {
if (err instanceof AndroidPackageNotDefinedError) {
err.message += `\n${learnMore(
'https://docs.expo.dev/workflow/configuration/'
)} about configuration with app.json/app.config.js`;
throw err; // breaks out of the loop to avoid failing again after continuation
}
Log.error(err);
if (process.env.DEBUG) {
throw err; // breaks out of the loop so we can get stack trace
@@ -154,7 +146,7 @@ export class ManageAndroid {
buildProfile: BuildProfile<Platform.ANDROID>
): Promise<GradleBuildContext | undefined> {
assert(ctx.hasProjectContext, 'createProjectContextAsync: must have project context.');
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile, ctx.vcsClient);
return await resolveGradleBuildContextAsync(ctx.projectDir, buildProfile);
}
private async runProjectSpecificActionAsync(
Loaded 100 of 211 files, more files were not shown because too many files have changed in this diff. Show more